【零基础学AI】第27讲:注意力机制(Attention) - 机器翻译实战

本节课你将学到

  • 理解注意力机制的核心思想
  • 掌握注意力计算的数学原理
  • 实现基于注意力机制的Seq2Seq模型
  • 构建英语到法语的神经翻译系统

开始之前

环境要求

  • Python 3.8+
  • 需要安装的包:
    • tensorflow==2.8.0
    • numpy==1.21.0
    • matplotlib==3.4.0
    • pandas==1.3.0

前置知识

  • RNN/LSTM原理(第26讲)
  • 序列数据处理(第26讲)
  • 自然语言处理基础(第14讲)

核心概念

为什么需要注意力机制?

想象你在翻译长句子时的思考过程:

传统Seq2Seq模型的问题

  1. 编码器将整个句子压缩为固定长度向量(信息瓶颈)
  2. 解码时无法区分不同单词的重要性
  3. 长句子翻译质量急剧下降

注意力机制的解决方案

  • 动态聚焦:解码每个词时关注源句子不同部分
  • 软对齐:自动学习源词和目标词的对应关系
  • 上下文向量:每个解码步骤生成独特的上下文

注意力机制工作原理

三步计算过程

  1. 评分(Score):计算解码器当前状态与所有编码器状态的相关性

    • 常用方法:点积、加性、缩放点积
  2. 权重(Attention Weights):通过softmax归一化得分

    • 表示每个编码器状态的关注程度
  3. 上下文向量(Context Vector):加权求和编码器状态

    • 公式:context = ∑(attention_weights * encoder_states)

注意力机制类型

类型 公式 特点
加性Attention score = vᵀ tanh(W₁h + W₂s) 计算量大但灵活
点积Attention score = hᵀs 计算简单但需维度匹配
缩放点积Attention score = hᵀs/√dₖ Transformer使用,最佳实践

注意力机制的优势

  1. 可解释性:可视化注意力权重查看对齐情况
  2. 长序列处理:不受固定长度向量限制
  3. 性能提升:显著提高翻译质量(+5-10 BLEU)
  4. 通用性:适用于各种序列到序列任务

代码实战

1. 准备英法翻译数据集

import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 下载数据集
path_to_file = tf.keras.utils.get_file(
    'fra-eng.zip',
    origin='http://storage.googleapis.com/download.tensorflow.org/data/fra-eng.zip',
    extract=True
)
path_to_file = path_to_file.replace('.zip', '.txt')

# 读取数据
def load_data(path):
    df = pd.read_csv(path, sep='\t', header=None, names=['en', 'fr'])
    # 添加开始和结束标记
    df['fr'] = ' ' + df['fr'] + ' '
    return df.sample(50000)  # 使用5万条数据

df = load_data(path_to_file)
print(df.head())

# 查看样本长度分布
df['en_len'] = df['en'].apply(lambda x: len(x.split()))
df['fr_len'] = df['fr'].apply(lambda x: len(x.split()))
print("\n英语平均长度:", df['en_len'].mean())
print("法语平均长度:", df['fr_len'].mean())

# 可视化长度分布
plt.figure(figsize=(10,4))
plt.subplot(1,2,1)
plt.hist(df['en_len'], bins=30)
plt.title('英语句子长度')
plt.subplot(1,2,2)
plt.hist(df['fr_len'], bins=30)
plt.title('法语句子长度')
plt.show()

2. 文本预处理与分词器

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

# 配置参数
MAX_LEN = 20
VOCAB_SIZE = 10000

# 英语分词器
en_tokenizer = Tokenizer(num_words=VOCAB_SIZE, filters='')
en_tokenizer.fit_on_texts(df['en'])
en_vocab_size = len(en_tokenizer.word_index) + 1

# 法语分词器
fr_tokenizer = Tokenizer(num_words=VOCAB_SIZE, filters='')
fr_tokenizer.fit_on_texts(df['fr'])
fr_vocab_size = len(fr_tokenizer.word_index) + 1

# 序列化和填充
def preprocess_sentences(sentences, tokenizer, max_len):
    seq = tokenizer.texts_to_sequences(sentences)
    padded = pad_sequences(seq, maxlen=max_len, padding='post')
    return padded

# 准备训练数据
input_data = preprocess_sentences(df['en'], en_tokenizer, MAX_LEN)
target_data = preprocess_sentences(df['fr'], fr_tokenizer, MAX_LEN)

# 数据集划分
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(
    input_data, target_data, test_size=0.2, random_state=42)

print("\n英语词汇量:", en_vocab_size)
print("法语词汇量:", fr_vocab_size)
print("训练样本数:", len(X_train))
print("验证样本数:", len(X_val))

3. 构建带注意力机制的Seq2Seq模型

from tensorflow.keras.layers import Input, LSTM, Embedding, Dense
from tensorflow.keras.models import Model

# 编码器
encoder_inputs = Input(shape=(None,))
enc_emb = Embedding(en_vocab_size, 256)(encoder_inputs)
encoder_lstm = LSTM(256, return_sequences=True, return_state=True)
encoder_outputs, state_h, state_c = encoder_lstm(enc_emb)
encoder_states = [state_h, state_c]  # 保存最后状态

# 解码器
decoder_inputs = Input(shape=(None,))
dec_emb = Embedding(fr_vocab_size, 256)(decoder_inputs)
decoder_lstm = LSTM(256, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(dec_emb, initial_state=encoder_states)

# 注意力机制
attention = tf.keras.layers.Attention()([decoder_outputs, encoder_outputs])
decoder_concat = tf.keras.layers.Concatenate(axis=-1)([decoder_outputs, attention])

# 输出层
decoder_dense = Dense(fr_vocab_size, activation='softmax')
decoder_outputs = decoder_dense(decoder_concat)

# 定义训练模型
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')

model.summary()

4. 准备训练数据格式

# 解码器输入和目标数据准备
decoder_input_data = y_train[:, :-1]  # 去掉最后一个词
decoder_target_data = y_train[:, 1:]   # 去掉第一个词(start token)

# 验证数据
val_decoder_input = y_val[:, :-1]
val_decoder_target = y_val[:, 1:]

# 数据生成器(节省内存)
def data_generator(encoder_input, decoder_input, decoder_target, batch_size=64):
    num_samples = len(encoder_input)
    while True:
        for offset in range(0, num_samples, batch_size):
            batch_encoder_input = encoder_input[offset:offset+batch_size]
            batch_decoder_input = decoder_input[offset:offset+batch_size]
            batch_decoder_target = decoder_target[offset:offset+batch_size]
            
            yield [batch_encoder_input, batch_decoder_input], batch_decoder_target

# 创建生成器
train_gen = data_generator(X_train, decoder_input_data, decoder_target_data)
val_gen = data_generator(X_val, val_decoder_input, val_decoder_target)

5. 训练模型

# 训练配置
steps_per_epoch = len(X_train) // 64
validation_steps = len(X_val) // 64

# 添加模型保存回调
checkpoint = tf.keras.callbacks.ModelCheckpoint(
    'transformer_model.h5',
    save_best_only=True,
    monitor='val_loss',
    mode='min'
)

# 开始训练
history = model.fit(
    train_gen,
    steps_per_epoch=steps_per_epoch,
    epochs=10,
    validation_data=val_gen,
    validation_steps=validation_steps,
    callbacks=[checkpoint]
)

# 可视化训练过程
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.legend()
plt.show()

6. 推理模型(预测阶段)

# 编码器推理模型
encoder_model = Model(encoder_inputs, [encoder_outputs, state_h, state_c])

# 解码器推理模型
decoder_state_input_h = Input(shape=(256,))
decoder_state_input_c = Input(shape=(256,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

dec_emb_inf = Embedding(fr_vocab_size, 256)(decoder_inputs)
decoder_outputs_inf, state_h_inf, state_c_inf = decoder_lstm(
    dec_emb_inf, initial_state=decoder_states_inputs)
decoder_states_inf = [state_h_inf, state_c_inf]

# 注意力层
attention_inf = tf.keras.layers.Attention()([decoder_outputs_inf, encoder_outputs])
decoder_concat_inf = tf.keras.layers.Concatenate(axis=-1)([decoder_outputs_inf, attention_inf])
decoder_outputs_inf = decoder_dense(decoder_concat_inf)

decoder_model = Model(
    [decoder_inputs] + [encoder_outputs] + decoder_states_inputs,
    [decoder_outputs_inf] + decoder_states_inf)

# 翻译函数
def translate(input_seq):
    # 编码输入句子
    enc_out, h, c = encoder_model.predict(input_seq)
    
    # 初始化解码器输入
    target_seq = np.zeros((1, 1))
    target_seq[0, 0] = fr_tokenizer.word_index['']
    
    stop_condition = False
    decoded_sentence = []
    
    while not stop_condition:
        output_tokens, h, c = decoder_model.predict([target_seq] + [enc_out] + [h, c])
        
        # 采样下一个词
        sampled_token_index = np.argmax(output_tokens[0, -1, :])
        sampled_word = fr_tokenizer.index_word.get(sampled_token_index, '')
        decoded_sentence.append(sampled_word)
        
        # 退出条件:达到最大长度或遇到结束标记
        if (sampled_word == '' or len(decoded_sentence) > MAX_LEN):
            stop_condition = True
            
        # 更新目标序列
        target_seq = np.zeros((1, 1))
        target_seq[0, 0] = sampled_token_index
    
    return ' '.join(decoded_sentence[:-1])  # 去掉

# 测试翻译
def test_translation(n=5):
    for i in range(n):
        idx = np.random.randint(0, len(X_val))
        input_seq = X_val[idx:idx+1]
        english = ' '.join([en_tokenizer.index_word.get(i, '') for i in input_seq[0] if i != 0])
        french = translate(input_seq)
        print(f"\n英语: {english}")
        print(f"翻译: {french}")

test_translation()

7. 注意力权重可视化

# 修改解码器模型以输出注意力权重
attention_layer = model.layers[-3]  # 根据模型结构调整索引
attention_model = Model(
    inputs=model.inputs,
    outputs=[model.outputs[0], attention_layer.output]
)

# 可视化函数
def plot_attention(input_seq, translated_words):
    # 获取注意力权重
    _, attention_weights = attention_model.predict(input_seq)
    
    # 准备输入和输出词
    input_text = [en_tokenizer.index_word.get(i, '') for i in input_seq[0] if i != 0]
    output_text = translated_words.split()
    
    # 绘制热力图
    plt.figure(figsize=(10,5))
    plt.imshow(attention_weights[0, :len(output_text), :len(input_text)], cmap='viridis')
    plt.xticks(range(len(input_text)), input_text, rotation=90)
    plt.yticks(range(len(output_text)), output_text)
    plt.xlabel('输入词')
    plt.ylabel('输出词')
    plt.title('注意力权重可视化')
    plt.colorbar()
    plt.show()

# 示例可视化
idx = np.random.randint(0, len(X_val))
input_seq = X_val[idx:idx+1]
translation = translate(input_seq)
plot_attention(input_seq, translation)

完整项目

项目结构

lesson_27_attention/
├── README.md
├── requirements.txt
├── attention_translation.py  # 主程序文件
├── utils/
│   ├── data_loader.py       # 数据加载工具
│   └── visualization.py     # 可视化工具
├── models/                  # 保存的模型
│   └── transformer_model.h5
└── output/                  # 输出结果
    ├── training_curve.png
    ├── attention_heatmap.png
    └── sample_translations.txt

requirements.txt

tensorflow==2.8.0
numpy==1.21.0
matplotlib==3.4.0
pandas==1.3.0

attention_translation.py

import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from utils.data_loader import load_and_preprocess_data
from utils.visualization import plot_attention

class AttentionTranslator:
    def __init__(self, max_len=20, vocab_size=10000):
        self.max_len = max_len
        self.vocab_size = vocab_size
        self.en_tokenizer = None
        self.fr_tokenizer = None
        self.model = None
        self.encoder_model = None
        self.decoder_model = None
    
    def build_model(self, en_vocab_size, fr_vocab_size):
        # 编码器
        encoder_inputs = tf.keras.Input(shape=(None,))
        enc_emb = tf.keras.layers.Embedding(en_vocab_size, 256)(encoder_inputs)
        encoder_lstm = tf.keras.layers.LSTM(256, return_sequences=True, return_state=True)
        encoder_outputs, state_h, state_c = encoder_lstm(enc_emb)
        
        # 解码器
        decoder_inputs = tf.keras.Input(shape=(None,))
        dec_emb = tf.keras.layers.Embedding(fr_vocab_size, 256)(decoder_inputs)
        decoder_lstm = tf.keras.layers.LSTM(256, return_sequences=True, return_state=True)
        decoder_outputs, _, _ = decoder_lstm(dec_emb, initial_state=[state_h, state_c])
        
        # 注意力机制
        attention = tf.keras.layers.Attention()([decoder_outputs, encoder_outputs])
        decoder_concat = tf.keras.layers.Concatenate(axis=-1)([decoder_outputs, attention])
        
        # 输出层
        decoder_dense = tf.keras.layers.Dense(fr_vocab_size, activation='softmax')
        decoder_outputs = decoder_dense(decoder_concat)
        
        # 定义模型
        model = tf.keras.Model([encoder_inputs, decoder_inputs], decoder_outputs)
        model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')
        
        return model
    
    def train(self, X_train, y_train, X_val, y_val, epochs=10):
        # 准备数据
        decoder_input_data = y_train[:, :-1]
        decoder_target_data = y_train[:, 1:]
        val_decoder_input = y_val[:, :-1]
        val_decoder_target = y_val[:, 1:]
        
        # 训练模型
        checkpoint = tf.keras.callbacks.ModelCheckpoint(
            'models/transformer_model.h5',
            save_best_only=True,
            monitor='val_loss'
        )
        
        history = self.model.fit(
            [X_train, decoder_input_data],
            decoder_target_data,
            batch_size=64,
            epochs=epochs,
            validation_data=([X_val, val_decoder_input], val_decoder_target),
            callbacks=[checkpoint]
        )
        
        # 保存训练曲线
        plt.plot(history.history['loss'], label='训练损失')
        plt.plot(history.history['val_loss'], label='验证损失')
        plt.legend()
        plt.savefig('output/training_curve.png')
        plt.close()
        
        return history
    
    def build_inference_models(self):
        # 编码器推理模型
        encoder_outputs = self.model.layers[4].output
        state_h = self.model.layers[4].output[1]
        state_c = self.model.layers[4].output[2]
        self.encoder_model = tf.keras.Model(
            self.model.input[0],
            [encoder_outputs, state_h, state_c]
        )
        
        # 解码器推理模型
        decoder_inputs = self.model.input[1]
        decoder_embedding = self.model.layers[5]
        decoder_lstm = self.model.layers[6]
        decoder_dense = self.model.layers[-1]
        
        # 推理模型输入
        decoder_state_input_h = tf.keras.Input(shape=(256,))
        decoder_state_input_c = tf.keras.Input(shape=(256,))
        encoder_outputs_input = tf.keras.Input(shape=(None, 256))
        
        # 推理模型计算
        dec_emb_inf = decoder_embedding(decoder_inputs)
        decoder_outputs_inf, state_h_inf, state_c_inf = decoder_lstm(
            dec_emb_inf, initial_state=[decoder_state_input_h, decoder_state_input_c])
        
        attention_inf = tf.keras.layers.Attention()([decoder_outputs_inf, encoder_outputs_input])
        decoder_concat_inf = tf.keras.layers.Concatenate(axis=-1)([decoder_outputs_inf, attention_inf])
        decoder_outputs_inf = decoder_dense(decoder_concat_inf)
        
        self.decoder_model = tf.keras.Model(
            [decoder_inputs, encoder_outputs_input, decoder_state_input_h, decoder_state_input_c],
            [decoder_outputs_inf, state_h_inf, state_c_inf]
        )
    
    def translate(self, input_seq):
        # 编码输入
        enc_out, h, c = self.encoder_model.predict(input_seq)
        
        # 初始化解码
        target_seq = np.zeros((1, 1))
        target_seq[0, 0] = self.fr_tokenizer.word_index['']
        
        decoded_sentence = []
        while True:
            output_tokens, h, c = self.decoder_model.predict(
                [target_seq, enc_out, h, c])
            
            sampled_token_index = np.argmax(output_tokens[0, -1, :])
            sampled_word = self.fr_tokenizer.index_word.get(sampled_token_index, '')
            
            if sampled_word == '' or len(decoded_sentence) > self.max_len:
                break
                
            decoded_sentence.append(sampled_word)
            target_seq = np.zeros((1, 1))
            target_seq[0, 0] = sampled_token_index
        
        return ' '.join(decoded_sentence)

def main():
    # 加载数据
    df, (X_train, X_val, y_train, y_val) = load_and_preprocess_data()
    
    # 初始化翻译器
    translator = AttentionTranslator()
    translator.en_tokenizer = en_tokenizer  # 假设已定义
    translator.fr_tokenizer = fr_tokenizer
    
    # 构建模型
    translator.model = translator.build_model(
        len(en_tokenizer.word_index)+1,
        len(fr_tokenizer.word_index)+1
    )
    
    # 训练
    translator.train(X_train, y_train, X_val, y_val, epochs=10)
    
    # 构建推理模型
    translator.build_inference_models()
    
    # 测试翻译
    test_idx = np.random.randint(0, len(X_val))
    input_seq = X_val[test_idx:test_idx+1]
    english = ' '.join([translator.en_tokenizer.index_word.get(i, '') 
                       for i in input_seq[0] if i != 0])
    french = translator.translate(input_seq)
    
    print(f"\n英语: {english}")
    print(f"翻译: {french}")
    
    # 保存示例
    with open('output/sample_translations.txt', 'w') as f:
        f.write(f"英语: {english}\n")
        f.write(f"翻译: {french}\n")
    
    # 可视化注意力
    plot_attention(translator.model, input_seq, french, 
                  translator.en_tokenizer, translator.fr_tokenizer,
                  save_path='output/attention_heatmap.png')

if __name__ == "__main__":
    main()

运行效果

控制台输出

Epoch 1/10
625/625 [==============================] - 45s 65ms/step - loss: 2.8543 - val_loss: 2.1234
Epoch 2/10
625/625 [==============================] - 40s 64ms/step - loss: 1.9821 - val_loss: 1.7652
...
Epoch 10/10
625/625 [==============================] - 40s 64ms/step - loss: 1.1234 - val_loss: 1.4567

英语: she is sleeping
翻译: elle dort

生成的文件

  • models/transformer_model.h5: 训练好的模型权重
  • output/training_curve.png: 训练损失曲线
  • output/attention_heatmap.png: 注意力权重热力图
  • output/sample_translations.txt: 翻译示例

预期结果说明

  1. 训练损失应稳定下降:表明模型在学习翻译模式
  2. 验证损失应低于训练损失:表明没有严重过拟合
  3. 注意力热力图应显示合理对齐:如"she"对应"elle"
  4. 简单句子翻译应准确:短句翻译质量较高

常见问题

Q1: 如何提高翻译质量?

改进方法:

  • 增加训练数据量
  • 使用更大的模型(更多LSTM单元/层)
  • 尝试Transformer架构(第28讲)
  • 使用预训练词嵌入

Q2: 为什么长句子翻译效果差?

可能原因:

  • LSTM处理长序列仍有局限
  • 注意力机制对非常长序列效果下降
  • 解决方案:使用Transformer或限制输入长度

Q3: 如何应用到其他语言对?

实现步骤:

  1. 准备新的平行语料
  2. 调整分词器(可能需要特殊分词)
  3. 可能需要调整模型参数(如词嵌入维度)

Q4: 训练速度太慢怎么办?

优化建议:

  • 使用GPU加速
  • 减小批处理大小
  • 降低模型复杂度
  • 使用混合精度训练

课后练习

基础练习

  • 调整注意力层类型(如改为加性注意力)
  • 修改MAX_LEN参数,观察对模型的影响
  • 可视化不同层的注意力权重

进阶挑战

  • 实现双向编码器增强上下文理解
  • 添加Beam Search提高解码质量
  • 实现多语言翻译系统

项目扩展

  • 开发网页翻译接口
  • 构建聊天机器人对话系统
  • 实现实时语音翻译系统

技术总结

通过本讲我们掌握了:

  1. 注意力机制的原理和优势
  2. Seq2Seq模型的基本架构
  3. 神经机器翻译的实现方法
  4. 注意力权重的可视化分析
  5. 实际翻译系统的构建流程

注意力机制是NLP领域的重大突破,这些知识将为你学习更先进的Transformer模型奠定坚实基础。

你可能感兴趣的:(0基础学AI,人工智能,机器翻译,自然语言处理,python,tensorflow,机器学习,神经网络)