Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Java——自定义对象作为Map的Key


自定义的HashKey类

什么样的自定义类可以用作HashMap的Key?

  • 实现了HashCode方法和equals方法的类

作为Key后的对象有什么要求?

  • 该对象的值不能再修改,否则将导致containsKey找不到已经存储的Key
  • 注意,Key值被修改后是无论如何都找不到的,因为hash对象变化导致hash方式变了
    • 能正确找到hash桶的对象与目标对象(修改后的值)不相等
    • 与对象与目标对象(修改后的值)相等的对象找不到正确的Hash桶
    • 除非将修改的值改回来

拓展

  • 如果是使用TreeMap,则不是考虑hashCode方法,而是其他方法

DL——RNN

本文将介绍一般RNN,RNN的变种GRU(门控循环单元)和LSTM(长短期记忆网络)等神经网络结构

  • LSTM原始论文: NIPS 2014, Sequence to Sequence Learning with Neural Networks
  • GRU原始论文: EMNLP 2014, Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation

一般的RNN

循环神经网络(Recurrent Neural Network,RNN)

  • 将神经单元展开后的示意图如下:

RNN与CNN优缺点比较

此处我们之比较 CNN 和 RNN 在序列预测问题中的优劣

  • 关于前馈神经网络:
    • 是一种最简单的神经网络
    • 各神经元分层排列,每个神经元只接受前一层的输入,输出到下一层,已知到输出层
    • 整个网络中没有反馈(后面的 backward 求梯度不能算反馈,这里指一次输入到输出计算各层间没有反馈,数据流只能从上一层到下一层单向流动)
    • 可以用一个有向无环图表示
  • CNN:
    • 是一种前馈神经网络(多层感知机),是多层感知机的变体
    • 采用固定大小的输入并生成固定大小的输出
    • CNN是图像和视频处理的理想选择
  • RNN:
    • 不是前馈神经网络,有环
    • 可以处理任意长度的输入输出
    • 使用时间序列信息,能用之前的状态影响下一个状态
    • RNN是文本和语音分析的理想选择
  • RNN在处理序列信息的缺点:
    • 对短期输入非常敏感,但是对长期输入不敏感
    • 难以处理长文本中长距离的单词间的关系

LSTM

长短期记忆网络(Long Short-Term Memory, LSTM)

  • 整体示意图如下:
  • 核心是”(Gate)门”结构
    • 其中Sigmoid是输出0到1的数值,描述每个部分有多少量可以通过当前门
    • 0表示拒绝所有,1表示接受所有
  • 多个门和状态共同作用,使LSTM能够有效捕捉长期依赖关系并避免梯度消失问题:
    • 细胞状态 \( C_t \)部分 :长期记忆,传递历史信息
    • 隐藏状态 \( h_t \)部分 :短期记忆,当前时间步的输出
    • 输入门 \( i_t \)部分 :控制新信息的加入
    • 遗忘门 \( f_t \)部分 :控制历史信息的保留
    • 输出门 \( o_t \)部分 :控制细胞状态对隐藏状态的影响
    • 候选细胞状态 \( \tilde{C}_t \)部分 :当前时间步的候选更新值

遗忘门(Forget Gate,\( f_t \) )

  • 作用 :控制前一个细胞状态 \( C_{t-1} \) 的遗忘程度:\(f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)\),输出在0到1之间,决定保留或丢弃多少历史信息

输入门(Input Gate,\( i_t \))和 候选细胞状态(Candidate Cell State,\( \tilde{C}_t \))

  • 输入门作用 :控制当前输入信息对细胞状态的更新程度, \(i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)\),输入门的输出在0到1之间,决定哪些新信息需要加入细胞状态
  • 候选细胞状态作用 :表示当前时间步的候选更新值, \(\tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C)\),用于更新细胞状态,候选细胞状态是当前输入和前一隐藏状态的非线性组合

细胞状态(Cell State, \( C_t \) )

  • 细胞状态 \( C_t \) 作用 :细胞状态是LSTM的核心,负责长期记忆的传递。\(C_t = f_t \cdot C_{t-1} + i_t \cdot \tilde{C}_t\),它像一个“传送带”,能够在时间步之间传递信息,并通过门控机制(遗忘门控制历史信息的保留,输入门控制新信息的加入)决定保留或丢弃哪些信息,细胞状态的变化相对平缓,能够捕捉长期依赖关系,通过遗忘门和输入门更新:

输出门(Output Gate,\( o_t \))和 隐藏状态(Hidden State,\( h_t \))

  • 输出门 \( o_t \) 作用 :控制细胞状态对当前隐藏状态的影响程度,\(o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)\),输出门的输出在0到1之间,决定细胞状态如何影响当前隐藏状态
  • 隐藏状态 \( h_t \) 作用 :隐藏状态是LSTM的短期记忆,表示当前时间步的输出,\(h_t = o_t \cdot \tanh(C_t)\),它基于细胞状态和当前输入生成(输出门控制细胞状态对当前隐藏状态的影响,生成当前时间步的输出),并传递到下一个时间步,隐藏状态是LSTM对外的输出(即 output_t),它既用于当前时间步的输出,也用于下一个时间步的计算
  • 特别说明 :LSTM中的第 \(t\) 步的输出不是 \(o_t\)(\(o_t\)是输出门,而不是输出), LSTM的通用输出就是 \(h_t\) 本身,但具体将LSTM应用到不同场景中时,可以接入不同的层来将 \(h_t\) 解码/转换为真实的输出 \(output_t = TransLayer(h_t)\)
  • 补充问题 :在文本翻译中,解码阶段,是否LSTM的 \(h_{t-1}\) 和 \(x_t\) 相等?一般不相等,因为解码时有:
    $$x_t = Embedding(output_{t-1}) = Embedding(TransLayer(h_{t-1}))$$
    • 比如在文本翻译中 \(h_{t-1}\) 是一个向量,而 \(output_{t-1}\) 是一个具体的 token,而上述公式中的 \(x_t \) 则一般是输入 \(input_t\)(\(= output_{t-1}\)) 经过 Embedding 后的向量

LSTM简单代码实现

  • LSTM的PyTorch实现Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 定义 LSTM 单元
    class LSTMCell(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(LSTMCell, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size
    # 定义门参数
    self.W_i = nn.Linear(input_size + hidden_size, hidden_size) # 输入门参数
    self.W_f = nn.Linear(input_size + hidden_size, hidden_size) # 遗忘门参数
    self.W_c = nn.Linear(input_size + hidden_size, hidden_size) # 候选记忆单元参数
    self.W_o = nn.Linear(input_size + hidden_size, hidden_size) # 输出门参数

    def forward(self, x, h_prev, c_prev):
    combined = torch.cat((x, h_prev), dim=1)
    i = torch.sigmoid(self.W_i(combined))
    f = torch.sigmoid(self.W_f(combined))
    c_tilde = torch.tanh(self.W_c(combined))
    o = torch.sigmoid(self.W_o(combined))
    c_next = f * c_prev + i * c_tilde
    h_next = o * torch.tanh(c_next)
    return h_next, c_next

GRU

门控循环单元(Gated Recurrent Unit, GRU)

  • 整体示意图及推导如下:

  • 特别说明 :上述公式中,没有明确包含输出 \(output_t\),GRU中的第 \(t\) 步的通用输出和LSTM一致,就是 \(h_t\) 本身,但具体将GRU应用到不同场景中时,可以接入不同的层来将 \(h_t\) 解码/转换为真实的输出 \(output_t = TransLayer(h_t)\)

GRU简单代码实现

  • GRU的PyTorch实现Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 定义 GRU 单元
    class GRUCell(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(GRUCell, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size

    # 定义门参数
    self.W_z = nn.Linear(input_size + hidden_size, hidden_size)
    self.W_r = nn.Linear(input_size + hidden_size, hidden_size)
    self.W_h = nn.Linear(input_size + hidden_size, hidden_size)

    def forward(self, x, h_prev):
    combined = torch.cat((x, h_prev), dim=1)
    z = torch.sigmoid(self.W_z(combined))
    r = torch.sigmoid(self.W_r(combined))
    h_tilde = torch.tanh(self.W_h(torch.cat((x, r * h_prev), dim=1)))
    h_next = (1 - z) * h_prev + z * h_tilde
    return h_next

GRU与LSTM对比

  • 二者提出的时间基本相同
  • 都是用来解决RNN不能处理长期依赖的问题
  • LSTM有三个”门”结构, GRU只有两个”门结构”
  • GRU较简单,参数比较少,不容易过拟合,LSTM 比较复杂一点,但是常用

附录:一个基于nn.LSTM的Decoder-Encoder翻译模型实现

  • 一个基于nn.LSTM的Decoder-Encoder翻译实现示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    import torch
    import torch.nn as nn
    import torch.optim as optim

    # 编码器
    class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(Encoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(input_size, hidden_size)
    self.lstm = nn.LSTM(hidden_size, hidden_size)

    def forward(self, input, hidden):
    embedded = self.embedding(input).view(1, 1, -1)
    output = embedded
    output, hidden = self.lstm(output, hidden)
    return output, hidden

    def initHidden(self):
    return (torch.zeros(1, 1, self.hidden_size),
    torch.zeros(1, 1, self.hidden_size))

    # 解码器
    class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
    super(Decoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(output_size, hidden_size)
    self.lstm = nn.LSTM(hidden_size, hidden_size)
    self.out = nn.Linear(hidden_size, output_size)
    self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
    output = self.embedding(input).view(1, 1, -1)
    output = torch.relu(output)
    output, hidden = self.lstm(output, hidden)
    output = self.softmax(self.out(output[0]))
    return output, hidden

    def initHidden(self):
    return (torch.zeros(1, 1, self.hidden_size),
    torch.zeros(1, 1, self.hidden_size))


    # 数据集
    source_vocab = {'hello': 0, 'world': 1, '!': 2}
    target_vocab = {'你好': 0, '世界': 1, '!': 2, '<EOS>': 3}
    input_size = len(source_vocab)
    output_size = len(target_vocab)
    hidden_size = 256

    encoder = Encoder(input_size, hidden_size)
    decoder = Decoder(hidden_size, output_size)

    # 合并编码器和解码器的参数
    params = list(encoder.parameters()) + list(decoder.parameters())

    # 创建一个优化器来更新所有参数
    optimizer = optim.SGD(params, lr=0.01)
    criterion = nn.NLLLoss()

    # 训练数据
    train_data = [
    (["hello", "world", "!"], ["你好", "世界", "!", "<EOS>"])
    ]

    # 训练
    for epoch in range(100):
    total_loss = 0
    for src, tgt in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    target_tensor = torch.tensor([target_vocab[word] for word in tgt])

    optimizer.zero_grad()

    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    loss = 0
    for di in range(len(target_tensor)):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1)
    decoder_input = topi.squeeze().detach()
    loss += criterion(decoder_output, target_tensor[di].unsqueeze(0))

    loss.backward()
    optimizer.step()
    total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
    print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_data)}')


    # 评估
    reverse_target_vocab = {idx: word for word, idx in target_vocab.items()}
    for src, _ in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    decoded_words = []
    for di in range(10):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1) # topk(1)函数在默认维度(最后一维)上抽取最大的1个数字,并返回其在这个维度上的index
    decoded_word = reverse_target_vocab[topi.item()]
    decoded_words.append(decoded_word)
    if decoded_word == '<EOS>':
    break
    decoder_input = topi.squeeze().detach()

    print(f'Input: {" ".join(src)}, Translation: {" ".join(decoded_words)}')

    # Epoch 10, Loss: 4.957141399383545
    # Epoch 20, Loss: 4.250643253326416
    # Epoch 30, Loss: 3.5705487728118896
    # Epoch 40, Loss: 2.95521879196167
    # Epoch 50, Loss: 2.438739538192749
    # Epoch 60, Loss: 2.025095224380493
    # Epoch 70, Loss: 1.698549509048462
    # Epoch 80, Loss: 1.438602328300476
    # Epoch 90, Loss: 1.2278329133987427
    # Epoch 100, Loss: 1.0538270473480225
    # Input: hello world !, Translation: 你好 世界 ! <EOS>

附录:一个基于 nn.GRU 的 Decoder-Encoder 翻译模型实现

  • 一个基于 nn.GRU 的 Decoder-Encoder 翻译实现示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    import torch
    import torch.nn as nn
    import torch.optim as optim

    # 编码器
    class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(Encoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(input_size, hidden_size)
    self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
    embedded = self.embedding(input).view(1, 1, -1)
    output = embedded
    output, hidden = self.gru(output, hidden)
    return output, hidden

    def initHidden(self):
    return torch.zeros(1, 1, self.hidden_size)

    # 解码器
    class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
    super(Decoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(output_size, hidden_size)
    self.gru = nn.GRU(hidden_size, hidden_size)
    self.out = nn.Linear(hidden_size, output_size)
    self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
    output = self.embedding(input).view(1, 1, -1)
    output = torch.relu(output)
    output, hidden = self.gru(output, hidden)
    output = self.softmax(self.out(output[0]))
    return output, hidden

    def initHidden(self):
    return torch.zeros(1, 1, self.hidden_size) # 这里LSTM是2个隐向量


    # 数据集
    source_vocab = {'hello': 0, 'world': 1, '!': 2}
    target_vocab = {'你好': 0, '世界': 1, '!': 2, '<EOS>': 3}
    input_size = len(source_vocab)
    output_size = len(target_vocab)
    hidden_size = 256

    encoder = Encoder(input_size, hidden_size)
    decoder = Decoder(hidden_size, output_size)

    # 合并编码器和解码器的参数
    params = list(encoder.parameters()) + list(decoder.parameters())

    # 创建一个优化器来更新所有参数
    optimizer = optim.SGD(params, lr=0.01)
    criterion = nn.NLLLoss()

    # 训练数据
    train_data = [
    (["hello", "world", "!"], ["你好", "世界", "!", "<EOS>"])
    ]

    # 训练
    for epoch in range(100):
    total_loss = 0
    for src, tgt in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    target_tensor = torch.tensor([target_vocab[word] for word in tgt])

    optimizer.zero_grad()

    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    loss = 0
    for di in range(len(target_tensor)):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1)
    decoder_input = topi.squeeze().detach()
    loss += criterion(decoder_output, target_tensor[di].unsqueeze(0))

    loss.backward()
    optimizer.step()
    total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
    print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_data)}')


    # 评估
    reverse_target_vocab = {idx: word for word, idx in target_vocab.items()}
    for src, _ in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    decoded_words = []
    for di in range(10):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1) # topk(1)函数在默认维度(最后一维)上抽取最大的1个数字,并返回其在这个维度上的index
    decoded_word = reverse_target_vocab[topi.item()]
    decoded_words.append(decoded_word)
    if decoded_word == '<EOS>':
    break
    decoder_input = topi.squeeze().detach()

    print(f'Input: {" ".join(src)}, Translation: {" ".join(decoded_words)}')

    # Epoch 10, Loss: 4.1139960289001465
    # Epoch 20, Loss: 2.4189529418945312
    # Epoch 30, Loss: 1.5635995864868164
    # Epoch 40, Loss: 1.0992240905761719
    # Epoch 50, Loss: 0.8163852691650391
    # Epoch 60, Loss: 0.6290442943572998
    # Epoch 70, Loss: 0.49852463603019714
    # Epoch 80, Loss: 0.4044916033744812
    # Epoch 90, Loss: 0.3349364101886749
    # Epoch 100, Loss: 0.2822900116443634
    # Input: hello world !, Translation: 你好 世界 ! <EOS>
  • 与LSTM实现的主要区别是,output, hidden = self.gru(output, hidden) 中输入和输出的 hiden 的数量不同,LSTM 包含两个向量(\(C_t\) 和 \(h_t\)),而 GRU 只包含一个 \(h_t\)

Sklearn——模型方法和参数使用笔记

本文不定期更新


模型的一般使用流程

  • init
  • fit(x_train, y_train)
  • predict(x_test)

模型输出预测概率

  • 方法名称: predict_proba(x_test)
  • 该方法对于预测时使用概率或者分数的算法来说直接返回概率值,对于不能返回概率的类来说一般返回交叉验证结果的平均值等
    • NB: 概率值
    • LR: 逻辑回归的分数
    • SVM: 交叉验证生成的平均值, 这里的结果与predict预测结果可能有偏差

关于参数

  • 待补充

Pandas——为DataFrame的某一列实行OneHot编码

为DataFrame的某一列实行OneHot编码


使用OneHotEncoder进行编码

  • 基本实现思路:

    • 生成一个OneHotEncoder对象
    • 取出对应的列并处理成N*1维的数组,用其训练OneHotEncoder对象并进行编码转换
    • 将新编码的数据生成为新的DataFrame对象
      • 为新的编码每一列生成新的列名称
      • 为新的每行索引赋值为原始DataFrame对应的索引
    • 按照列合并两个DataFrame
    • 删除之前的列
  • 实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from sklearn.preprocessing import OneHotEncoder

    def one_hot_for_column(df, column):
    """
    encode column of df with one hot method
    :param df: an object of DataFrame
    :param column: the name of column in df
    :return: new object of DataFrame and object of OneHotEncoder
    """
    ohe = OneHotEncoder()

    # ohe.fit(df[column].values.reshape(-1, 1))
    # col_series = ohe.transform(df[column].values.reshape(-1, 1)).toarray()
    # <==>
    col_series = ohe.fit_transform(df[column].values.reshape(-1, 1)).toarray()

    columns = ["%s_%s" % (column, str(m)) for m in range(1, col_series.shape[1] + 1)]
    sub_df = pd.DataFrame(col_series, columns=columns, dtype=int, index=df.index)
    new_df = pd.concat([df, sub_df], axis=1)
    new_df.drop(columns=column, inplace=True)
    return new_df, ohe

NLP——词嵌入的前世今生

词嵌入的发展,NNLM,CBOW,skip-gram
词嵌入(Word Embedding)也称为词向量(Word2Vec): 指的是将词转换成向量的形式


基本问题

  • 用语言模型做预训练
    • 语言模型: 如何计算一段文本序列在某种语言下出现的概率?
  • 如何表示文本中的一个词?

文本表示模型

词袋模型

  • One-Hot: 向量维度与词的数量相同,某个维度为1,其他都为0
  • TF-IDF: 计算词对文档的贡献(权重)
  • TextRank: 类似于PageRank的思想
    • 如果一个单词出现在很多单词后面的话,那么说明这个单词比较重要
    • 一个TextRank值很高的单词后面跟着的一个单词,那么这个单词的TextRank值会相应地因此而提高
优缺点
  • OneHot:
    • 维度灾难
    • 语义鸿沟
  • TF-IDF和TextRank
    • [个人理解]也无法表示语义,所以存在语义鸿沟

主题模型

  • LSA: 矩阵分解(SVD)的方式,又名 Latent Semantic Indexing
  • pLSA: 简单的概率隐式语义模型
  • LDA: 无监督聚类常用的模型,有Gibbs Sampling和变分推断等实现,网上有很多资料,也有很多实现
  • L-LDA: LDA的有监督版本,核心思想就是在LDA吉布斯实现的版本基础上加上采样限制,使得每次采样只能控制在对应主题上,效果比较好,网上没有官方的实现,大部分实现不完全,感兴趣可以试试我的实现Labeled-LDA-Python
    • L-LDA理论上可以得到比LDA质量更高,效果更好的词向量
优缺点:
  • LSA:
    • 利用全局语料特征
    • 但SVD求解计算复杂度大

基于词向量的固定表征

  • Word2vec: 基于上下文训练, 拥有相同上下文的词相近,基于分布式假设(相同上下文语境的词有似含义)
  • FastText: 通过对”词-词”共现矩阵进行分解从而得到词表示的方法,基于分布式假设(相同上下文语境的词有似含义)
  • GloVe: 基于全局预料,结合了LSA和word2vec的优点, 论文: GloVe: Global Vectors for Word Representation
优缺点
  • 目前提到的以上所有都是静态词向量,无法解决一次多义等问题
  • Word2vec和FastText:
    • 优化效率高,但是基于局部语料
    • 本质上二者也都是语言模型
  • GloVe:
    • 基于全局语料库、并结合上下文语境构建词向量, 结合了LSA和word2vec的优点
    • 可以看作是更换了目标函数和权重函数的全局word2vec

基于词向量的动态表征

  • ELMo:
    • 采样 LSTM 提取特征
    • 采用双向语言模型
    • 实际上是两个单向语言模型(方向相反)的拼接,这种融合特征的能力比BERT一体化融合特征方式弱
  • GPT:
    • 采样 Transformer 进行特征提取
    • 采用单向语言模型
  • BERT:
    • 采用Transformer进行特征提取
    • 采用双向语言模型
优缺点
  • 三者都是基于语言模型的动态词向量,能解决一次多义问题
  • ELMo:
    • 采用1层静态向量+2层 LSTM,多层提取能力有限
  • GPT和BERT:
    • Transformer 可采用多层,并行计算能力强
  • 很多任务表明:
    • Transformer 特征提取能力强于 LSTM
  • GPT和BERT都采用 Transformer,Transformer 是 encoder-decoder 结构,GPT 的单向语言模型采用 decoder 部分,decoder 的部分见到的都是不完整的句子;BERT 的双向语言模型则采用 encoder 部分,采用了完整句子

NNLM/RNNLM

  • 词向量为副产物,存在效率不高等问题;

不同模型的对比

word2vec vs NNLM

  • 本质上都可以看做是语言模型
  • 对 NNLM 来说,目不是词向量,是语言模型
  • word2vec 虽然本质上也是语言模型,但是更关注词向量本身,因此做了很多优化来提高计算效率
    • 与 NNLM 相比,词向量直接 sum(CBOW),而不是拼接,并取消隐藏层
    • 考虑到 Softmax 需要遍历整个词汇表,采用 Hierarcal Softmax和 Negative Sampling 进行优化
    • word2vec 所做的一切都是为了一切为了快速生成词向量

word2vec vs fastText

  • 都可以进行无监督学习, fastText 训练词向量时会考虑 subword(子词)
    • 在 fastText 中,每个中心词被表示成子词的集合,下面我们用单词 “where” 作为例子来了解子词是如何产生的
    • 首先,我们在单词的首尾分别添加特殊字符“<”和“>”以区分作为前后缀的子词
    • 然后,将单词当成一个由字符构成的序列来提取 n 元语法
      • 例如,当n=3时,我们得到所有长度为3的子词:“<wh>” “whe” “her” “ere” “<re>”以及特殊子词“<where>”
  • fastText还可以进行有监督学习进行文本分类:
    • 结构与CBOW类似,但学习目标是人工标注的分类结果, 而不是中心词 \(w\)
    • 采用hierarchical softmax对输出的分类标签建立哈夫曼树,样本中标签多的类别被分配短的搜寻路径;
    • 引入N-gram,考虑词序特征;
    • 引入subword(子词)来处理长词,处理未登陆词问题

word2vec vs GloVe vs LSA

  • word2vec vs GloVe
    • 语料库:
      • word2vec是局部语料库训练的,特征提取基于滑动窗口,可以在线学习
      • GloVe的滑动窗口是为了建立”词-词”共现矩阵,需要统计共现概率,不能在线学习
    • 无监督学习:
      • word2vec是无监督学习
      • GloVe通常被认为是无监督学习(无需标注),但实际上GloVe还是有label的,即共现次数 \(log(X_{ij})\)
    • 损失函数:
      • word2vec损失函数实质上是带权重的交叉熵,权重固定;
      • GloVe的损失函数是最小平方损失函数,权重可以做映射变换
    • 总结:
      • word2vec的损失函数是带权重的交叉熵 , 权重是固定的
      • GloVe可以看作是目标函数为MSE ,权重函数可变的全局word2vec
  • GloVe vs LSA
    • LSA(Latent Semantic Analysis)可以基于co-occurance matrix构建词向量,实质上是基于全局语料采用SVD进行矩阵分解,然而SVD计算复杂度高;
    • GloVe可看作是对LSA一种优化的高效矩阵分解算法,采用Adagrad对最小平方损失进行优化;

ELMo、GPT、BERT

  • 三者使用了两种不同的NN组件提取特征
    • ELMo: LSTM
    • GPT, BERT: Transformer
  • 三者使用了两种不同的语言模型:
    • GPT: 单向语言模型
    • ELMo, BERT: 双向语言模型
    • ELMo实际上是两个方向相反的单向语言模型的拼接, 融合特征的能力比BERT那种一体化的融合特征方式弱
    • GPT和BERT都采用Transformer,Transformer是 Encoder-Decoder结构
      • GPT为单向语言模型,采用的是Decoder部分, Decoder部分见到的都是不完整的句子
      • BERT为双向语言模型,采用的是Encoder部分,采用了完整的句子

下面是一些模型的详细介绍


NNLM

模型目标

  • 给定一段长度为m的序列时,预测下一个词(第m+1)个词出现的概率
  • 参考博客DL——NNLM

word2vec

  • 此处只做简单介绍,更详细的讲解请参考博客word2vec学习笔记
  • 一个比较好的手写笔记版:通俗易懂讲解Word2vec的本质 - 对白的文章 - 知乎
  • 损失函数: 带权重的交叉熵损失函数(权重固定)

两类实现

  • 一般实现: 每次训练一个样本 \((w_{in}, w_{out})\),需要计算所有词和当前词一起对应的 softmax, 很耗时间,需要 \(O(V)\) 复杂度的时间
  • 优化实现: Hierarcal Softmax 和 Negative Sampling 两种方式
    • Hierarcal Softmax: 每次训练一个样本 \((w_{in}, w_{out})\),只需要更新平均 \(O(log V)\) 个非叶子节点(每个非叶子结点就是一个词向量)即可
    • Negative Sampling: 每次训练一个样本 \((w_{in}, w_{out})\),只需要更新 \({w_j|w_j\in {w_O}\bigcup W_{neg}}\) 个词向量, 通常负样本的集合大小取 \(log(V)\) 量级甚至更小
层次 Softmax 模型框架
  • 层次 Softmax(Hierarchical Softmax),也称为层次化 Softmax
  • 对于词表大小为 \(V\) 的场景,如果使用普通的 Softmax,则每次需要在分母上计算 \(V\) 次指数运算并求和,使用 层次 Softmax 后转化为 \(\log(V)\) 次二分类任务
  • 核心思想: 输出层由语料库中词出现的频数当作权值构造出的哈夫曼树作为输出
  • 引入哈夫曼树 , Hierarcal Softmax是一种有效计算Softmax的方式,使用二叉树来表示
  • 其中Hierarchical Softmax模型的输出层由语料库中词出现的频数当作权值构造出的哈夫曼树作为输出
  • CBOW
    • 输入层: \(2c\)个词向量
    • 投影层: \(2c\)个词向量的累加
    • 输出层: 哈夫曼树(重点是词w所在的叶子节点, 以及w到根节点的路径)
      • 所有单词没有输出向量表示形式,每个内部结点有一个输出向量 \(v\)
      • 输出层共有 \(V - 1\) 个非叶节点, 也就是要学习 \(V - 1\) 个输出向量
      • 看起来并没有什么优化,但是每次迭代训练一个样本 \(w_{in}, w_{out}\) 时,我们只需要优化从根节点到输出单词 \(w_{out}\) 的路径上的输出向量即可, 平均共 \(O(log V)\) 个向量
      • 每个单词的概率计算需要考虑叶子节点来求出
    • 传统softmax公式 :在传统的softmax函数中,计算目标词与所有词汇之间的概率分布公式为\(P ( w_t | C ) = \frac{e^{v_{w_t}^T v_{w_c} } }{\sum_{w’} e^{v_{w’}^T v_{w_c} } }\),其中\(w_t\)是目标词,\(w_c\)是上下文词,\(v_{w_t}\)和\(v_{w_c}\)是目标词和上下文词的词向量
      • 其中:\(v_{w_c}\) 和 \(v_{w_t}\) 就是我们要学习的词向量
    • 层次softmax公式 :给定一个目标词\(w_t\),上下文词\(w_c\),通过霍夫曼树计算从目标词到上下文词的路径概率。假设从根节点到叶节点的路径上有多个节点,对于目标词\(w_t\),路径\(P\)到达目标词,要最大化的概率公式为\(P ( w_t | C ) = \prod_{k = 1}^{L} \sigma(v_{w_t}^T v_{n_k})\)。其中\(L\)是路径的长度(即路径上的节点数),\(n_k\)是路径上的第\(k\)个节点(非叶子节点),\(v_{w_t}\)是目标词的词向量,\(v_{n_k}\)是路径节点\(n_k\)的词向量。每一个路径的概率通过sigmoid函数计算:\(\sigma(x) = \frac{1}{1 + e^{-x} }\)
      • 其中:\(v_{w_t}\) 就是我们要学习的词向量
  • Skip-gram
    • 输入层:词 \(w\) 的向量
    • 投影层:依旧是词 \(w\) 的向量
    • 输出层:哈夫曼树(重点是词 \(w\) 的上下文窗内 \(2c\) 个词所在的叶子节点,以及各自到根节点的路径)
Negative Sampling模型框架
  • 实际上就是简单的对每一个样本中每一个词都进行负例采样(本质上就是用负采样代替了层次softmax的哈弗曼树)
  • 负采样是噪声对比估计的一个简化版本,目的是提高训练速度并改善所得词向量的质量
  • 核心思想: 在每次迭代过程中, 有大量的输出向量需要更新,为了解决这一困难, Negative Sampling的做法是只更新其中一部分输出向量
  • 利用不同词在语料中出现的频次多少来决定被采样到的概率(单词出现的频次越大,越可能被选做负样本)
  • 负采样: 利用不同词在语料库中出现的频次多少来决定该词被采样到的概率
  • 每次训练一个样本 \((w_{in}, w_{out})\),只需要更新 \({w_j|w_j\in {w_O}\bigcup W_{neg}}\) 个词向量, 通常负样本的集合大小取 \(log(V)\) 量级甚至更小
  • CBOW
    • 输入层: \(2c\)个词向量
    • 投影层:\(2c\)个词向量的累加
    • 输出层:负采样词集(重点是词w的负词词集的参数 \(\theta\),负词的概率永远是1-Sigmoid函数值)
  • Skip-gram模型:
    • 输入层:词w的向量
    • 投影层:依旧是词w的向量
    • 输出层:每个上下文词u的负采样(重点是词w的负词词集的参数 \(\theta\),负词的概率永远是1-Sigmoid函数值)
总结
  • 层次Softmax模型:
    • CBOW模型的一次更新是:
      • 输入 \(2c\) 个词向量的累加(简单的sum,而不是拼接)
      • 对中心词 \(w\) 上的路径节点系数进行更新(输出层)
      • 对所有的上下文词的词向量进行整体一致更新(输入层到隐藏层的参数, 修改多个词向量)
    • Skip-gram模型的一次更新是:
      • 输入中心词 \(w\) 的词向量(直接输入,无其他操作)
      • 对每个上下文词 \(u(i)\) 所在的路径上的节点系数进行更新(输出层,修改多个上下文的结点)
      • 对词w的词向量进行单独更新(输入层到隐藏层的参数,只修改一个词向量)
    • CBOW一次训练更新计算量小,Skip-gram一次训练计算量大(更新的系数更多?)
  • CBOW看起来更新的更平滑,适合小量文本集上的词向量构建,Skip-gram每次更新都更加有针对性,所以对于大文本集上表现更好

word2vec的缺陷

  • 不能解决多义词问题(只能把几种意思同时编码进向量里)
    • 这是所有固定词向量的通病
    • 解决方案是动态词向量(允许不同上下文使用不同词向量表达同一个词),包括ELMo, GPT, BERT

GPU——显卡相关知识

  • 参考链接:
    • 大模型GPU性能与性价比天梯图详解!各类GPU的大模型训练与推理性能对比,以及主流GPU性价比分析!

显卡性能详细天梯图

  • 显卡性能天梯图(截止到 24 年底):
  • H100 SXM 和 H100 性能不一致的原因主要是接口不同导致的,SXM 英伟达设计的一种集成到主板上的接口,支持更大功率,能发挥的显卡性能也更好
  • 图中没有显示的常见显卡简单说明:
    • H800 是 H100 的替代卡,通过降低互联带宽(从 H100 的 900GB/s 降至 450GB/s)规避出口限制,但算力仍达行业顶尖水平
      • 23 年腾讯开始使用
    • H20 是 25年新发布的 H100 限制版芯片,目前有 96G 和 141G 版本,带宽 4TB/s (高于 H100 的 3.35TB/s) 性能大概只有

AI 相关显卡对比

  • 注意:本图来自网络(不一定符合真实情况)
  • 文字总结:
    • 华为
        1. Ascend 910C:A100 的 2.56 倍
        1. Ascend 950DT:A100 的 1.6 倍
        1. Ascend 910B:A100 的 1.026 倍
        1. Ascend 910:A100 的 0.82 倍
    • 寒武纪
        1. Siyuan 690:A100 的 1.8倍
        1. Siyuan 590:A100 的 0.9 倍
        1. Siyuan 290:A100 的 0.82倍
    • 海光
        1. BW100(DCU3):A100 的 1.12 倍
        1. K100(DCU2):A100 的 0.32 倍
        1. Z100(DCU1):A100 的 0.26倍
    • 沐曦
        1. C500:A100 的 0.77 倍
        1. MXN100:A100 的 0.26 倍
    • 壁仞
        1. BR106M/BR106B:A100 的 0.54 倍
        1. BR106C:A100 的 0.41 倍
    • 摩尔线程
        1. MTT S4000:A100 的 0.31 倍
        1. MTT S3000:A100 的 0.1倍。

主要显卡对照记录

  • 主要显卡对照记录表格
    型号 发布时间 架构 主要身份 / 卖点 备注
    H100 2022 年 Q3 Hopper 首款 Hopper 架构旗舰,取代 A100 80 GB HBM3,989 TFLOPS FP16,是 2022 年的“卡皇”
    H800 2023 年 Q1 Hopper 中国特供“缩水版 H100” 带宽砍到 2 TB/s,算力基本保留
    H200 2023-11-13 Hopper H100 的“显存升级版” 141 GB HBM3e + 4.8 TB/s,推理速度大约 2×H100
    H20 2023-11 Ampere(部分文献亦标为 Hopper 降规) 中国特供“再缩水版”,算力只有 H100 的 1/7 96 GB HBM3、148 TFLOPS FP16,对标昇腾 910B
  • 注:H20 虽官方 PPT 仍写 Hopper,但算力/显存规格与 H100/H200 差距过大,业内多视为“Ampere 时代”最后一款中国特供卡
  • H910 系列显卡
    • 亲自测试结论:
      • 910B 约是 H800 的 1/4
      • 910C 约是 H800 的 1/3?

附录:GPU、NPU 和 TPU 辨析

  • NPU(Neural Processing Unit,神经网络处理器)是专门为加速人工智能和深度学习任务设计的硬件芯片
    • NPU 采用针对神经网络算法优化的指令集和硬件结构,如高效矩阵乘法单元、低精度数据处理能力,能以低功耗实现高性能计算,支持边缘智能,可在本地处理数据
    • NPU 主要应用于智能手机、智能安防、自动驾驶、医疗等领域,如手机 AI 拍照、实时人脸识别等
    • 华为的升腾 910B 就是 NPU
  • TPU(Tensor Processing Unit,张量处理单元)是谷歌为加速机器学习工作负载,特别是 TensorFlow 框架下的深度学习任务而定制开发的专用集成电路
    • TPU 采用定制化架构,针对张量运算优化,有高效矩阵乘法单元和专用内存结构,能在较低功耗下实现极高计算性能
    • TPU 主要应用于大规模数据中心的深度学习训练和推理任务
  • GPU(Graphics Processing Unit,图形处理器)是一种专门用于处理图形和图像相关运算的微处理器
    • GPU 拥有大量流处理器,可并行处理多个线程,最初用于图形渲染,如游戏中的3D场景渲染等
    • 因 GPU 强大的并行计算能力,也广泛应用于深度学习、科学计算、影音编辑和渲染等领域

GitHub——主题推荐算法

Topic Suggestions for Millions of Repositories

Github官网原文


总体流程图


分为七个步骤:


Readme 预处理与清除

(Readme preprocessing and cleanup)

  • 移除不要的文本块(Remove unwanted blocks)
    • 去除没用文本:一些块是没用的,比如code, table 和image链接等
  • 文本划分(Text segmentation)
    • 提取有用的文本:一个启发式的README tagger,借助格式分析:分析缩进(indentation),空格(spacing),和反撇号( `, backtick)等决定是否是有用信息【说明:这里语法分析是没必要的,我们只关心有用的文本,其他的都是为噪音(noise)】
    • 提取到有用文本后,删除拓展部分:HTML标签,路径和其他处理出来的部分等
    • 最后将剩下的文本进行粗粒度分割【用标点符号(punctuation marks)和README中的一些符号,比如临近哈希符号(contiguous hash symbols)】

生成候选主题

(Generate candidate topics)

  • 用自定义的停用词去将词单元划分出来(Use custom stop words to break text into word units)
    • 停用词去除
    • n-gram分段小于等于4(测试发现小于4的比较好,太长的主题过于具体了,不合适)

移除噪音主题

(Eliminate noisy topics)

  • 用逻辑回归模型删减“bad”主题(Use a logistic regression model to prune “bad” topics)
    • 监督逻辑回归模型(supervised logistic regression model)主要针对除了频数比较小的外,一些不好的,比如”great”, “cool”等,这里的模型是一个分类模型,分为两类(good[positive] and bad(negative)) , 我们称之为关键词过滤模型
      • 手动收集大约300个数据集作为训练集
      • 单个动词一般都是Bad类请教师兄
      • 其他的(Other features we used were occurrence of user names, n-gram size of a phrase, length of a phrase, and numeric content within a phrase.)
      • 以后打算加入回馈机制(来自用户的)去更新这个关键词过滤模型:接受度高的词作为positive的,接受度低的作为停用词或者negative的
  • 移除不满足最小频数的主题(Eliminate topics not satisfying minimum frequency count)

给主题评分

(Score Topics)

  • 用混合tf-idf分数,以主题频率和n元词作为打分标准(Use combination of tf-idf scores, topic frequency and n-gram size for scoring)
    • 评估多种指标后,选择了点互信息指标(PMI): Pointwise Mutual Information
    • 考虑另一种指标tf-idf:参考论文Using TF-IDF to Determine Word Relevance in Document Queries.pdf或者Python的实现
    • The second approach we tried uses the average tf-idf scores of individual words in a phrase weighted by the phrase frequency (if it’s more than one word long) and n-gram size.
    • 两种方法比较:
      • PMI: 强调独特性,越特殊的短语评分越高,但有些可能只是拼写错误(Typo)或者是没有被删除的代码片段
      • tf-idf: 不强调独特性,最终选择是tf-idf,原因是这个指标较好的平衡了独特性和主题与仓库(repository)的相关程度
    • 在tf-idf的基础上还添加了一下其他的比如boosting scores等方法
    • 下面是TF-IDF的说明*

规范化主题

(Canonicalize topics)

  • 使用内部词典规范主题形式(Use an in-house dictionary to suggest canonical form of topics)

    • 解决文字层面的差别和变化等,比如下面四个主题

      neural-network
      neural-networks
      neuralnetwork
      neuralnetworks


移除相似的主题

(Eliminate near similar topics)

  • 用基于Jaccard相似性评分的贪心算法(Greedy approach using Jaccard similarity scoring)

    • motivation:在得到Top-N的主题后,有些主题其实很相似,虽然都有用,但是他们只是在不同粒度描述了同一个主题而已,因此我们需要删除一些重复的,比如下面的例子

      machine learning
      deep learning
      general-purpose machine learning
      machine learning library
      machine learning algorithms
      distributed machine learning
      machine learning framework
      deep learning library
      support vector machine
      linear regression

    • method: 两个短语的相似性计算使用的是基于词的Jaccard相似性(两个短语的差集与并集的比值,因为它对较短的短语很有效,而且分数是[0-1]的,很方便设置阈值(thresholds)),用贪心算法,如果两个主题相似,去除分数较低的那一个,上面的例子去除相似主题后的结果是:

      machine learning
      deep learning
      support vector machine
      linear regression


返回Top-K主题作为最终结果

Go语言——String与Slice深度解析

Note: 他们都是struct类型的


String

type StringHeader struct {
    Data uintptr
    Len  int
}

Slice

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

把Slice转换为String的语法为

[]byte("string")
string([]byte{1,2,3})

注意:这种实现会有拷贝操作


如何避免拷贝操作呢?

答案是自己实现指针转换(也可用反射实现头部转换),省去复制数据部分,同时注意这种实现后底层的数据不能再更改了,不然容易引发错误

直接修改指针类型并构建相应头部

func String2Slice(s string) []byte {
    sp := *(*[2]uintptr)(unsafe.Pointer(&s))
    bp := [3]uintptr{sp[0], sp[1], sp[1]}
    return *(*[]byte)(unsafe.Pointer(&bp))
}

func BytesToString(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

使用反射机制获取到头部再进行转换

func Slice2String(b []byte) (s string) {
    pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
    pstring.Data = pbytes.Data
    pstring.Len = pbytes.Len
    return
}


func String2Slice(s string) (b []byte) {
    pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
    pbytes.Data = pstring.Data
    pbytes.Len = pstring.Len
    pbytes.Cap = pstring.Len
    return
}

Go语言——struct类型作为函数参数

本文先给出Go语言中struct类型作为函数参数传入的例子,然后给出总结


首先看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func changeValue(person Person){
person.name = "zhoujiahong"
person.age = 22
}

func changePoint(person *Person){
person.name = "zhoujiahong"
person.age = 22
}
func main() {
person := Person{
name:"origin",
}
changeValue(person)
fmt.Printf("Person:\n person.name: %s---person.age: %d\n", person.name, person.age)
changePoint(&person)
fmt.Printf("*Person:\n person.name: %s---person.age: %d\n", person.name, person.age)
}

//output:
//Person:
// person.name: origin---person.age: 0
//*Person:
// person.name: zhoujiahong---person.age: 22

总结

  • struct类型的传递是按值传递的,与数组[5]byte等的传递相似,也可以理解为和C++一样
  • struct类型传递与切片不同,切片事实上是一个指针(栈)指向一块数组(堆)这样的数据结构,所以无论如何都是传入指针或者是指针(栈)的指针(指向栈)
  • 可以理解为: 切片类似于Java(切片还多了个特色,通过传入切片可以修改指针(栈)本身,而Java是做不到的), 而数组类似于C++

注意:struct类型与切片传值方式不同而与数组相同

Go语言——切片和数组的区别

关于切片与数组的区别,这里给出定义和传值等使用上的区别,最后给出总结


数组定义

  • 申明类型定义
1
2
var arr [2]byte
arr[0] = 10
  • 直接赋值定义
1
arr := [2]byte{1,2}

切片定义

  • 申明类型定义
1
var sli []byte
  • 直接赋值定义
1
sli := make([]byte, 5)

作为参数传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

func changeSlicePoint(p *[]byte){
(*p)[0] = 100
}

func changeSlice(a []byte){
a[4] = 100
}

func changeArrayPoint(p *[5]byte){
(*p)[0] = 100
}

func changeArray(a [5]byte){
a[4] = 100
}

func main() {
array := [5]byte{1,2,3,4,5}
var sli1 []byte
sli1 = make([]byte, 5)
slice := []byte{1,2,3,4,5}

changeSlicePoint(&sli1)
changeSlice(sli1)
changeArray(array)
changeArrayPoint(&array)
changeSlicePoint(&slice)
changeSlice(slice)

fmt.Printf("slice[0]: %d\nslice[4]: %d\n", slice[0], slice[4])
fmt.Printf("sli1[0]: %d\nsli1[4]: %d\n", sli1[0], sli1[4])
fmt.Printf("array[0]: %d\narray[4]: %d\n", array[0], array[4])
}


//output:
//slice[0]: 100
//slice[4]: 100
//sli1[0]: 100
//sli1[4]: 100
//array[0]: 100
//array[4]: 5
//

总结

  • 切片传入时是按照引用传递的,加上指针甚至可以修改引用(内存地址)本身

    比如下面的代码会是的传入的引用重新指向nil指针:

1
2
3
4
func changeSlicePoint(p *[]byte){
(*p)[0] = 100
*p = nil
}
  • 数组的传递是按值传递的,使用了指针可实现传入地址,从而实现对数组的修改
1…515253…64
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

631 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4