# NLP 自然语言处理

# 常见任务

  1. 文本分类:情感分析(积极 / 消极)、垃圾邮件识别、新闻主题分类【句子级别】
  2. 序列标注:命名实体识别(找人名、地名、手机号)、文本生成、信息抽取、文本转化【Token 级别】

# 文本表示

# 分词

  1. 词级分词:将文本按照词切分在英语中空格往往是天然的切词标志,但是容易出现 OOV 问题(未登录词问题)
  2. 字符级分词:一个字母、数字、标点甚至空格,都会被视作一个独立的 token,不会有 OOV 问题,但模型必须依赖更长的上下文来推断词义和结构,这显著增加了建模难度和训练成本。
  3. 子词级分词:将词语切分为更小的单元 —— 子词(subword),例如词根、前缀、后缀或常见词片段。

以上是英文适用的分词方法,下面是中文适用的分词方法。

  1. 字符级分词:一个字就进行一次切分,比英文中的字符分词,中文的字符分词更加 “语义友好”。
  2. 词级分词:由于中文没有空格等天然词边界,词级分词通常依赖词典、规则或模型来识别词语边界。
  3. 子词级分词:它们以汉字为基本单位,通过学习语料中高频的字组合(如 “自然”、“语言”、“处理”),自动构建子词词表。在当前主流的中文大模型(如通义千问、DeepSeek)中,子词分词已成为广泛采用的文本切分策略。

# JiebBa 分词组件

word_gen = jieba.cut("我的名字叫Karry,来自计算机科学技术学院")
for word in word_gen:
    print(word)



名字

Karry

来自
计算机
科学技术
学院

# 词表示

  1. One-hot 编码(独热编码):它将词汇表中的每个词映射为一个稀疏向量,向量的长度等于整个词表的大小。该词在对应的位置为 1,其他位置为 0。在实际自然语言处理任务中,one-hot 表示已经很少被直接使用。
  2. 语义化词向量:它通过对大规模语料的学习,为每个词生成一个具有语义意义的稠密向量表示。比如 “女人” 和 “女孩”,这两个词向量就很接近。Word2Vec。

# Word2Vec

1

左侧是 CBOW,中间词是教师,以此来学习上下文。

右侧是 Skip-gram,上下文是教师,以此来学习中间词。

# GENSIM 词向量组件

# 加载与使用公开词向量

from gensim.models import KeyedVectors
model_path = 'sgns.weibo.word.bz2'
model = KeyedVectors.load_word2vec_format(model_path)
similarity = model.similarity('公交', '地铁')
print('公交和地铁的相似度', similarity)

公交和地铁的相似度 0.65458214

model.most_similar(positive=['男人', '女孩'], negative=['男孩'], topn=10)

男人 + 女孩 - 男孩 = 女人

[(‘女人’, 0.6578881740570068),
(‘女孩子’, 0.515068531036377),
(‘女生’, 0.4519447982311249),
(‘女人真’, 0.44206273555755615),
(‘女人们’, 0.4369858503341675),
(‘女人爱’, 0.435453325510025),
(‘寡言少语’, 0.42479249835014343),
(‘男孩子’, 0.42177465558052063),
(‘看女人’, 0.41949161887168884),
(‘笨女人’, 0.4182003140449524)]

# 训练自己的词向量

import pandas as pd
import jieba
from gensim.models import Word2Vec
comments = pd.read_csv('./data/online_shopping_10_cats.csv', encoding='utf-8')
reviews = comments['review']
reviews = reviews.dropna()
sentences = [[token for token in jieba.__lcut(review) if token.strip() != ''] for review in reviews]
model = Word2Vec(
    sentences,            # 已分词的句子序列
    vector_size=100,      # 词向量维度
    window=5,             # 上下文窗口大小
    min_count=2,          # 最小词频(低于将被忽略)
    sg=1,                 # 1:Skip-Gram,0:CBOW
    workers=4             # 并行训练线程数
)
model.wv.save_word2vec_format('./data/word2vec.txt')

# 词向量的应用

import torch
from torch import nn
from gensim.models import KeyedVectors
# 加载词向量
wv = KeyedVectors.load_word2vec_format('./data/word2vec.txt')
# 构建词向量矩阵
num_embedding = len(wv.key_to_index)
embedding_dim = wv.vector_size
embedding_matrix = torch.randn(num_embedding, embedding_dim)
for word, index in wv.key_to_index.items():
    embedding_matrix[index] = torch.from_numpy(wv[word])
print(embedding_matrix.shape)
# 创建 Embedding
embedding = nn.Embedding.from_pretrained(embedding_matrix)
text = '我喜欢乘坐地铁'
tokens= jieba.lcut(text)
input_ids = [wv.key_to_index[token] for token in tokens if token in wv.key_to_index]
input_tensor = torch.LongTensor(input_ids)
embedding(input_tensor)

# ELMo 模型 —— 一词多义问题

NNLM 模型是在预测下一个词,而词向量是副产品。

Word2Vec 模型是在专门做词向量,有 CBOW 和 Skip-gram。

ELMo 模型解决的是一词多义的问题。

1

ELMo 不仅仅是训练了一个 Q 矩阵,还把这个词的上下文信息融入到这个 Q 矩阵中,左边的 LSTM 获取 E2 的上文信息,右侧的 LSTM 获取下文信息。

T1 包含了第一个词的特征,与此同时也包含了语法特征和语义特征。

然而 LSTM 无法并行计算,因此我们引入了注意力机制。

# RNN 基本结构

RNN 以时间步为基本单位,逐个处理每一个 Token,新的隐藏状态是由,上一个隐藏状态与当前步共同决定的。

1

x1,x2,x3…xt 这是一组特征,但是这是与时间有关的特征,hₜ是每时每刻的预测。

比如一句话:“我出生在中国,我会说中文”

我们可以按时间步展开:

时间步 输入 xₜ(词向量) 隐藏状态 hₜ(编码了前面的上下文) 可能的预测 yₜ(下一个词)
t=1 “我” h₁(编码了 “我”) “出生”
t=2 “出生” h₂(编码了 “我 出生”) “在”
t=3 “在” h₃(编码了 “我 出生 在”) “中国”
t=4 “中国” h₄(编码了 “我 出生 在 中国”) “,”
t=5 “,” h₅(编码了 “我 出生 在中国 ,”) “我”
t=6 “我” h₆(编码了前半句 +“我”) “会”
t=7 “会” h₇(编码了前半句 +“我 会”) “说”
t=8 “说” h₈(编码了前半句 +“我 会 说”) “中文”

RNN 的隐藏状态 hₜ 起到了 **“记忆”** 的作用:

  • 当模型看到 “中国” 时,h₄ 已经编码了 “我出生在中国” 这个信息。
  • 当模型看到后半句 “我会说中文” 时,h₈ 已经编码了整句话的上下文,因此它可以推断出 “中文” 是合理的下一个词,因为 “出生在中国” 和 “会说中文” 之间有语义关联

在 RNN 中,每个词 xₜ 是随时间输入的特征隐藏状态 hₜ 是模型对 “到目前为止所有词” 的理解,而预测 yₜ 是基于这种理解做出的下一步判断

# RNN 与 FCNN(全连接神经网络)的区别

1

左侧是 RNN,右侧是全连接神经网络。全连接神经网络是一个直筒的结构,RNN 是一个与时间有关的循环结构。

# FCNN 的展开

1

# RNN 的展开

1

# 为什么?

FCNN 把每个输入当作独立静态向量

RNN 把输入看成随时间展开的序列,用共享参数 + 循环隐状态来显式建模 “过去” 对 “现在” 的影响。

# 数学表达

ht=tanh(xtWx+ht1Wh+b),\begin {array}{c} h_t = \tanh(x_t W_x + h_{t-1} W_h + b), \end{array}

[ht1ht2ht3ht4]=tanh([ht11ht12ht13ht14][wh11wh12wh13wh14wh21wh22wh23wh24wh31wh32wh33wh34wh41wh42wh43wh44]+[xt1xt2xt3xt4][wx11wx12wx13wx14wx21wx22wx23wx24wx31wx32wx33wx34]+[b1b2b3b4])\begin {array}{c} \begin{bmatrix} h_t^1 & h_t^2 & h_t^3 & h_t^4 \end{bmatrix} = \tanh( \begin{bmatrix} h_{t-1}^1 & h_{t-1}^2 & h_{t-1}^3 & h_{t-1}^4 \end{bmatrix} \begin{bmatrix} w_h^{11} & w_h^{12} & w_h^{13} & w_h^{14} \\ w_h^{21} & w_h^{22} & w_h^{23} & w_h^{24} \\ w_h^{31} & w_h^{32} & w_h^{33} & w_h^{34} \\ w_h^{41} & w_h^{42} & w_h^{43} & w_h^{44} \end{bmatrix} + \begin{bmatrix} x_t^1 & x_t^2 & x_t^3 & x_t^4 \end{bmatrix} \begin{bmatrix} w_x^{11} & w_x^{12} & w_x^{13} & w_x^{14} \\ w_x^{21} & w_x^{22} & w_x^{23} & w_x^{24} \\ w_x^{31} & w_x^{32} & w_x^{33} & w_x^{34} \end{bmatrix} + \begin{bmatrix} b_1 & b_2 & b_3 & b_4 \end{bmatrix} ) \end{array}

# 权重共享

权重共享” 是 RNN 与 CNN 里最常被提到的一个核心设计,但它不是 “所有神经元共用同一个数”,而是同一组参数(矩阵 / 向量)在多个位置、多个时间步或空间位置反复使用

# 双向 RNN

双向递归神经网络(Bidirectional Recurrent Neural Network,简称 Bi-RNN)是一种特殊的递归神经网络(RNN),它在处理序列数据时,不仅考虑了过去的信息,还考虑了未来的信息。这使得 Bi-RNN 在处理诸如自然语言处理(NLP)任务时特别有用,因为它能够同时利用上下文信息。

# 在 PyTorch 使用 RNN

import torch.nn as nn
# 双层双向 RNN
rnn = nn.RNN(input_size=3, hidden_size=4, num_layers=2, batch_first=True, bidirectional=True)
# shape: (batch, seq_len, input_size)
input = torch.randn(2, 4, 3)
output, hn = rnn(input)
print(output.shape)
print(hn.shape)

torch.Size([2, 4, 8])
torch.Size([4, 2, 4])

# 词嵌入层 API 应用

# 作用

把词或词对应的索引转为词向量

# 使用框架实现

import torch
import jieba
import torch.nn as nn
def dm01():
    text = "当您登录平台即表示您接受本隐私政策的全部内容,并同意平台按本政策收集、使用和保护您的相关个人信息。"
    words = jieba.lcut(text)
    print(words)
    '''
    len(words) : 词表大小
    embedding_dim : 词向量维度
    '''
    embed = nn.Embedding(len(words), embedding_dim=4)
    for i, word in enumerate(words):
        # 词索引(张量)转变为词向量
        word2Vec = embed(torch.tensor([i]))
        print(word, word2Vec)
if __name__ == '__main__':
    dm01()

# RNN 的 API 使用

RNN 就像是你的大脑,在看电影的过程中记住剧情。

import torch
import torch.nn as nn
'''
input_size:词向量的维度
hidden_size:隐藏状态的维度
num_layers:隐藏层数
batch_first:批次优先
'''
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=1)
'''
5:有5个词
3:3个批次
10:每个词的细节是10维度
'''
x = torch.rand(5, 3, 10)
'''
1:隐藏层层数
3:句子数量
20:有20个循环维度,也就是隐藏状态的维度
'''
h0 = torch.zeros(1, 3, 20)
'''
RNN处理
x:本次的输入
h0:上一次的隐藏状态
输出
output:每个时间步的输出,包含了所有时间步骤的隐藏状态
h1:最后一次隐藏状态,大脑里最新的剧情
'''
output, h1 = rnn(x, h0)
print(f'output_shape:{output.shape}')
print(f'h1_shape:{h1.shape}')

# 智能提示输入法的实现

# 最佳实践

print(__file__)

__ file __ 是当前文件的绝对路径,py 在处理相对路径时可能会遇到问题,最好使用绝对路径。

from pathlib import Path
print(Path(__file__).parent.parent / "data/raw/synthesized_.jsonl")

所以我们通常会这样写:

RAW_DATA_DIR = Path(__file__).parent.parent / "data/raw"
def process():
    print("开始处理数据")
    # 读取原始文件
    df = pd.read_json(RAW_DATA_DIR / "synthesized_.jsonl", lines=True, orient='records',
                      encoding='utf-8')
    print(df.head())
    print("数据处理完成")

# 数据预处理

import jieba
from sklearn.model_selection import train_test_split
import pandas as pd
from tqdm import tqdm
import config
def build_dataset(sentences, word2index, desc="构建数据集"):
    indexed_sentences = [[word2index.get(token, 0) for token in jieba.lcut(sentences)] for sentences in
                         sentences]
    dataset = []
    for index, sentence in tqdm(enumerate(indexed_sentences), desc=desc):
        for i in range(len(sentence) - config.SWQ_LEN):
            input = sentence[i:i + config.SWQ_LEN]
            target = sentence[i + config.SWQ_LEN]
            dataset.append({
                'input': input,
                'target': target,
            })
    return dataset
def process():
    print("开始处理数据")
    # 读取原始文件
    df = pd.read_json(config.RAW_DATA_DIR / "synthesized_.jsonl", lines=True, orient='records',
                      encoding='utf-8').sample(frac=0.1)
    # 提取句子
    sentences = []
    for sentencesX in df['dialog']:
        for sentencesXs in sentencesX:
            sentences.append(sentencesXs.split(':')[1])
    print(f"句子的数量为:{len(sentences)}")
    # 划分数据集
    train_sentences, valid_sentences = train_test_split(sentences, test_size=0.2)
    # 构建词表
    vocab_set = set()
    for sentence in tqdm(train_sentences, desc='构建词表'):
        vocab_set.update(jieba.lcut(sentence))
    vocab_list = ['<unk>'] + list(vocab_set)
    print(f"词表的数量为:{len(vocab_list)}")
    # 保存此表
    with open(config.MODELS_DIR / "vocab.txt", 'w', encoding='utf-8') as f:
        f.write('\n'.join(vocab_list))
    # 构建训练集
    word2index = {word: index for index, word in enumerate(vocab_list)}
    train_dataset = build_dataset(train_sentences, word2index)
    # 保存训练集
    pd.DataFrame(train_dataset).to_json(config.PROCESSED_DATA_DIR / "train.jsonl", orient='records', lines=True)
    # 构建验证集
    valid_dataset = build_dataset(valid_sentences, word2index)
    # 保存验证集
    pd.DataFrame(valid_dataset).to_json(config.PROCESSED_DATA_DIR / "valid.jsonl", orient='records', lines=True)
    print("数据处理完成")
if __name__ == '__main__':
    process()

# 数据集

import pandas as pd
import torch
from torch.utils.data import DataLoader, Dataset
from torch.utils.data.dataset import _T_co
from src.NLP.SmartPrompt.src import config
# 定义 Dataset
class InputMethodDataset(Dataset):
    def __init__(self, path):
        super().__init__()
        self.data = pd.read_json(path, lines=True, orient='records').to_dict('records')
    def __getitem__(self, index) -> _T_co:
        input_tensor = torch.tensor(self.data[index]['input'], dtype=torch.long)
        target_tensor = torch.tensor(self.data[index]['target'], dtype=torch.long)
        return input_tensor, target_tensor
    def __len__(self) -> int:
        return len(self.data)
# 提供一个获取 DATALoader 的方法
def get_dataloader(train=True):
    path = config.PROCESSED_DATA_DIR / ("train.jsonl" if train else "valid.jsonl")
    dataset = InputMethodDataset(path)
    return DataLoader(dataset, batch_size=config.BATCH_SIZE, shuffle=True)
if __name__ == '__main__':
    train_loader = get_dataloader(train=True)
    valid_loader = get_dataloader(train=False)
    print(len(train_loader))
    print(len(valid_loader))
    for input_tensor, target_tensor in train_loader:
        print(input_tensor.shape)
        print(target_tensor.shape)
        break

# 训练

import time
import torch
from torch import nn
from torch.utils.tensorboard import SummaryWriter
from tqdm import tqdm
from src.NLP.SmartPrompt.src import config
from src.NLP.SmartPrompt.src.dataset import get_dataloader
from src.NLP.SmartPrompt.src.model import InputMethodModel
def train_onr_epoch(model, dataloader, loss_fn, optimizer, device):
    """
    :param model: 模型
    :param dataloader: 数据集
    :param loss_fn: 损失函数
    :param optimizer: 优化器
    :param device: 设备
    :return: 损失值
    """
    model.train()
    total_loss = 0
    for batch in tqdm(dataloader, desc="训练中"):
        features, targets = batch
        features = features.to(device)
        targets = targets.to(device)
        outputs = model(features)
        loss = loss_fn(outputs, targets)
        total_loss += loss.item()
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    return total_loss / len(dataloader)
def train():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    dataloader = get_dataloader(train=True)
    with open(config.MODELS_DIR / 'vocab.txt', 'r', encoding='utf-8') as f:
        vocab_list = f.readlines()
        vocab_list = [vocab.strip() for vocab in vocab_list]
    model = InputMethodModel(len(vocab_list)).to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
    writer = SummaryWriter(log_dir=config.LOGS_DIR/time.strftime('%Y-%m-%d_%H-%M-%S'))
    bestLoss = float('inf')
    for epoch in range(1, 1 + config.EPOCHS):
        print("-" * 5, f"Epoch {epoch}/{config.EPOCHS}", "-" * 5)
        loss = train_onr_epoch(model, dataloader, loss_fn, optimizer, device)
        print(f"loss:{loss}")
        writer.add_scalar('loss', loss, epoch)
        if loss < bestLoss:
            bestLoss = loss
            torch.save(model.state_dict(), config.MODELS_DIR / 'model.pt')
    writer.close()
if __name__ == '__main__':
    train()

# 模型

from torch import nn
import config
class InputMethodModel(nn.Module):
    def __init__(self, vocab_size, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=config.EMBEDDING_DIM)
        self.rnn = nn.RNN(
            input_size=config.EMBEDDING_DIM,
            hidden_size=config.HIDDEN_SIZE,
            batch_first=True,
        )
        self.linear = nn.Linear(in_features=config.HIDDEN_SIZE, out_features=vocab_size)
    def forward(self, x, *args, **kwargs):
        embedding = self.embedding(x)
        output, hidden = self.rnn(embedding)
        last_hidden_state = output[:,-1,:]
        output = self.linear(last_hidden_state)
        return output

# 预测

import jieba
import torch
from src.NLP.SmartPrompt.src import config
from src.NLP.SmartPrompt.src.model import InputMethodModel
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
with open(config.MODELS_DIR / 'vocab.txt', 'r', encoding='utf-8') as f:
    vocab_list = f.readlines()
    vocab_list = [vocab.strip() for vocab in vocab_list]
word2index = {word: index for index, word in enumerate(vocab_list)}
index2word = {index: word for index, word in enumerate(vocab_list)}
model = InputMethodModel(len(vocab_list)).to(device)
model.load_state_dict(torch.load(config.MODELS_DIR / 'model.pt'))
def predict(text):
    tokens = jieba.lcut(text)
    input = [word2index.get(token, 0) for token in tokens]
    input_tenosr = torch.tensor([input], dtype=torch.long).to(device)
    model.eval()
    with torch.no_grad():
        output = model(input_tenosr)
    topk = torch.topk(output, k=5).indices.tolist()
    return [index2word[index] for index in topk[0]]
if __name__ == '__main__':
    user_str = ''
    while user_str != 'q':
        now_str = input("请输入:")
        user_str += now_str
        if now_str.strip() == 'q':
            break
        top5_tokens = predict(user_str)
        print(top5_tokens)
        print("当前输入:", user_str)

# LSTM

LSTM 灵感来原与计算机逻辑门。

1

图片来源于:LSTM - 长短期记忆递归神经网络 - 知乎

遗忘门:主要负责遗忘过去的记忆信息,最重要的是要去结合当下的状态信息去处理。

输入门:输入门主要负责现在要记下什么,可以看到在输入门的右侧还有一个 tanh 的激活函数,

输出门:

1

# 双向结构

1

# 注意力机制

注意力机制的起源,是因为我们往往会聚焦于重要的信息。

怎么做?

我(查询对象 Q),这张图(被查询对象 V)

我看一眼这张图,我就会去判断哪些东西对我而言是重要的,那些东西对我来说是是不重要的(去计算 Q 和 V 里的事物重要程度)。

图片来自:https://www.cnblogs.com/nickchen121/p/16470710.html

注意力机制

# Transformer

111

Transformer 其实就是 Attention 的一个堆叠。

# Transformer 的 Encoder

Transformer 的 Encoder 由 N 层堆叠的结构(通常是 6 层) 组成,每一层的结构几乎一样,每层包含两个主要子层:

输入 → [多头自注意力机制] → [前馈神经网络] → 输出

并且每个子层后都有:

  • 残差连接(Residual Connection)
  • 层归一化(Layer Normalization)

RNN:逐个时间步处理序列,有记忆但慢。

Transformer Encoder:一次性处理全序列,通过注意力机制 “并行感知” 所有词之间的关系,速度快、效果好。

说白了还是在弄一个词向量,只不过这个词向量更加优秀!!

# Transformer 的 Decoder

解码器接收编码器生成的词向量,然后通过这个词向量生成翻译的结果。

# 参考文献

  1. 【[手把手教学] 基于 RNN、LSTM 神经网络单特征用电负荷预测】https://www.bilibili.com/video/BV1HN4y1Y7Lt
  2. 【尚硅谷 NLP 教程,nlp 自然语言处理,Transformer、LSTM、BERT 等大模型技术全覆盖】https://www.bilibili.com/video/BV1k44LzPEhU
  3. 【Word2Vec 模型、Attention、Transformer 】https://space.bilibili.com/383551518
  4. 【Transformer 模型详解(图解最完整版)】https://zhuanlan.zhihu.com/p/338817680
  5. 【黑马程序员 AI 大模型《神经网络与深度学习》】https://www.bilibili.com/video/BV1c5yrBcEEX
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

KarryLiu 微信支付

微信支付

KarryLiu 支付宝

支付宝