02.26 GPT-2沒什麼神奇的,PyTorch 就可以復現代碼

歡迎來到「帶註釋的 GPT-2」。

我讀過的最精彩、解釋最清楚的文章之一是「The Annotated Transformer」https://nlp.seas.harvard.edu/2018/04/03/attention.html。它引起了前所未有的關注,一個簡單的想法就是用一個文件註釋你所需要的代碼。

我在機器學習方面的經驗讓我意識到,當你將事情寫成代碼的時候,其實現和秘密會變得更清晰,而不再是魔法了。

魔法沒有什麼神奇的。魔術師只是理解一些簡單的東西,而這些東西對未經訓練的觀眾來說似乎並不簡單。一旦你學會了操作魔術卡片,你也可以變魔術。

——Jeffrey Friedl 在《Mastering Regular Expressions》一書中寫道

GPT-2 一開始看起來像是魔術,它的看起來太美麗了,但希望我能為你解釋魔術,在你讀完這篇文章時揭示所有的技巧,這是我的目標。使那些熱衷於理解 GPT-2 模型是如何工作的人更好理解。

注:幾乎所有代碼都是從Hugging Face(https://github.com/huggingface/transformers/blob/master/src/transformers/modeling_gpt2.py)的 GPT-2 實現中複製、啟發和引用的,只保留了簡單的基本要素。如果你想在並行 GPU 上訓練 GPT-2 模型,在微調時保存檢查點,在多個 CPU 上運行推理任務等等,我建議你使用 Hugging Face API。最近,Hugging Face 發佈了一個簡單的教程,它教你如何做到這一點:https://huggingface.co/blog/how-to-train。

在這篇文章中,我並不是在試圖重新發明輪子,而是僅僅把一系列已經存在的優秀資源放在一起,讓讀者更容易掌握 GPT-2。我讓讀者在他們所選擇的任何領域進一步建立這些基礎。

你不能在弱小的基礎上建造一座偉大的建築。如果你要有一個強大的上層建築,你必須有堅實的基礎。

——Gordon B. Hinckley

學習本教程的先決條件

本文假設讀者對注意力機制和 tansformers 有著紮實的理解。GPT-2 採用 12 層的,僅有解碼器的 transformer 架構。如果你想複習一下或瞭解注意力機制和 transformers,這裡有一個很好的資源列表:

  • Jay Alammar 的 The illustrated Transformer:http://jalammar.github.io/illustrated-transformer/

  • 哈佛大學 The Annotated Transformer:https://nlp.seas.harvard.edu/2018/04/03/attention.html

  • Rachel Thomas 和 Jeremy Howard的 Introduction to the Transformer:https://www.youtube.com/watch?v=AFkGPmU16QA&list=PLtmWHNX-gukKocXQOkQjuVxglSDYWsSh9&index=18&t=0s

如果你剛剛開始你的 NLP 之旅,或者你是一個專家,我絕對會推薦 Rachel Thomas 和 Jeremy Howard 教授的 fast.ai NLP 課程(https://www.fast.ai/2019/07/08/fastai-nlp/)。本課程從基礎開始,包括使用樸素貝葉斯和 Logistic 迴歸進行語義分類,接著是 RNN,後面還討論了遷移學習、ULMFiT、Seq2Seq 翻譯和 transformers 等。它是 fast.ai 團隊免費提供的優秀資源。

另一個關於 GPT-2 本身的優秀資源,是 Jay Alammar 的 The Illustrated GPT-2(http://jalammar.github.io/illustrated-gpt2/)。本文從語言模型的基本介紹開始,以一種非常容易理解的方式逐步解釋 GPT-2 模型。我強烈建議讀者閱讀這篇文章。

哈佛大學 The Annotated Transformer 使用 PyTorch 實現了完整的 transformer 架構,是深入理解 transformer 的好方法。

然後,讓我們在這些優秀的現有資源的基礎上,用代碼實現 GPT-2 吧~

摘要

自然語言處理任務,如問答、機器翻譯、閱讀理解等,通常是在特定任務的數據集上進行有監督的學習。我們證明,當語言模型在一個名為 WebText 的數百萬網頁的新數據集上訓練時,它開始學習這些任務,而不需要任何明確的監督。我們最大的模型,GPT-2,是一個 1.5B 參數的 transformer,它可以獲得最先進的語言建模成果,但仍然不適合 WebText。模型中的示例反映了這些改進,幷包含連貫的文本段落。這些發現為構建語言處理系統提供了一條有希望的途徑,該系統可以從自然發生的演示中學習執行任務。

Zero-shot 設置是不微調語言模型並直接在目標數據集上運行推理的設置。例如,在 WebText 上預覽一個 LM,並直接嘗試預測 Amazon 影評數據集的下一個單詞。

模型架構(GPT-2)

我們的 LM 使用基於 transformer 的架構。該模型主要遵循 OpenAI GPT 模型的細節,並進行了一些修改。層規範化被移動到每個子塊的輸入,類似於預激活剩餘網絡,並且在最終的自關注塊之後添加了額外的層規範化。我們在初始化時將剩餘層的權重按 1/√N 的因子進行縮放,其中 N 是剩餘層的數量。詞彙量擴大到 50257 個單詞。我們還將上下文大小從 512 增加到 1024 個,並使用更大的批大小——512。

模型規格(GPT)

我們的模型基本上遵循了最初 transformer 的工作原理。我們訓練了一個 12 層的只解碼的 transformer,它有隱藏的自注意力頭(768 維狀態和 12 個注意力頭)。對於位置前饋網絡,我們使用了 3072 維的內部狀態。我們使用 Adam 優化方案,最大學習速率為 2.5e-4。學習速率在前 2000 次更新中從零線性增加,並使用餘弦調度將其退火為 0。我們在 64 個隨機抽樣的小批量、512 個令牌的連續序列上訓練了 100 個階段。由於 layernorm 在整個模型中廣泛使用,簡單的 N(0,0.02)權重初始化就足夠了。我們使用了一個 bytepair 編碼(BPE)詞彙表。我們還採用了在中提出的 L2 正則化的改進版本,在所有非偏倚或增益權重上的 w=0.01。對於激活函數,我們使用高斯誤差線性單位(GELU)。

GPT-2没什么神奇的,PyTorch 就可以复现代码

導入

import torch

import copy

import torch.nn as nn

import torch.nn.functional as F

from torch.nn.modules import ModuleList

from torch.nn.modules.normalization import LayerNorm

import numpy as np

import os

from tqdm import tqdm_notebook, trange

import logging

logging.basicConfig(level = logging.INFO)

logger = logging.getLogger

GPT-2 內部的 transformer 解碼器

要重用用於描述 transformer 的術語,注意是一個查詢(Q)和一組鍵(K)和值(V)對的函數。為了處理更長的序列,我們修改了 transformer 的多頭自注意力機制,通過限制 Q 和 K 之間的點積來減少內存使用:

注意力是查詢、鍵和值的組合

class Conv1D(nn.Module):

def __init__(self, nx, nf): super.__init__

self.nf = nf

w = torch.empty(nx, nf)

nn.init.normal_(w, std=0.02)

self.weight = nn.Parameter(w)

self.bias = nn.Parameter(torch.zeros(nf))

def forward(self, x):

size_out = x.size[:-1] + (self.nf,)

x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight)

x = x.view(*size_out)

return x

CONV1D 層解釋

CONV1D 層本身可以看作是一個線性層。本質上,它是投射一個初始張量 x(最終尺寸為 x.size(-1))並傳遞給它,最終尺寸為 self.nf。

下面是相同的輸出示例:

d_model = 768

conv1d = Conv1D(d_model, d_model*3)

x = torch.rand(1,4,d_model) #represents a sequence of batch_size=1, seq_len=4 and embedding_sz=768, something like "Hello how are you"

x = conv1d(x)

x.shape

>> torch.Size([1, 4, 2304])

如上例所示,CONV1D 返回的張量的最終維數是初始大小的 3 倍。我們這樣做是為了能夠將輸入轉換為查詢、鍵和值矩陣。

然後可以檢索查詢、鍵和值矩陣,如下所示:

query, key, value = x.split(d_model, dim=-1)

query.shape, key.shape, value.shape

>> (torch.Size([1, 4, 768]), torch.Size([1, 4, 768]), torch.Size([1, 4, 768]))

將輸入轉換為 Q、K 和 V 矩陣的另一種方法是必須有單獨的 Wq、Wk 和 Wv 矩陣。我已經在這篇文章底部的附加部分解釋了這一點。我發現這種方法更直觀、更具相關性,但在本文中我們使用了 CONV1D 層,因為我們重用了 Hugging Face 的 CONV1D 預訓練權重。

前向層解釋

class FeedForward(nn.Module):

def __init__(self, dropout, d_model=768, nx=768*4):

super.__init__

self.c_fc = Conv1D(d_model, nx)

self.c_proj = Conv1D(nx, d_model)

self.act = F.gelu

self.dropout = nn.Dropout(dropout)

def forward(self, x):

return self.dropout(self.c_proj(self.act(self.c_fc(x))))

在 Jay Alammar 的文章中有一個很好的解釋,也就是上面提到的,輸入是如何先經過注意力層,然後再進入前向層的。前饋網絡是一個正常的網絡,它接受來自注意力層(768)的輸出,將其投射到 nx(768×4)維,添加一個激活函數 self.act(GELU),將其投射回 d_model (768) 並添加 dropout(0.1)。

注意力層解釋

下面的摘錄是從論文上摘取的:https://arxiv.org/abs/1706.03762。

標度點產品注意力

我們稱我們的注意力為「標度點產品注意力」。輸入包括維度 dk 的查詢和鍵以及維度 dv 的值。我們使用所有鍵計算查詢的點積,用√dk除以每個鍵,然後應用 softmax 函數獲得值的權重。

GPT-2没什么神奇的,PyTorch 就可以复现代码

在實際應用中,我們同時計算一組查詢的注意力函數,將它們組合成一個矩陣 Q,並將鍵和值組合成矩陣 K 和 V。我們將輸出矩陣計算為:

輸出矩陣為 Q、K 和 V 的組合

最常用的兩個注意力函數是加性注意力函數和點積(乘法)力函數注意。除了比例因子 1/√dk 外,點積注意力與我們的算法相同。附加註意力使用具有單個隱藏層的前饋網絡計算兼容性函數。雖然二者在理論複雜度上相似,但在實際應用中,點積注意力速度更快,空間效率更高,因為它可以使用高度優化的矩陣乘法碼來實現。當 dk 值較小時,兩種機制的表現相似,但在 dk 值較大時,加性注意力優於點積注意力。我們懷疑,對於 dk 的較大值,點積在數量上增長較大,將 softmax 函數推入具有極小梯度的區域。為了抵消這一影響,我們將網點產品縮放至 1/√dk。

為了在代碼中實現注意力層,我們首先利用 CONV1D 層,得到前面解釋的 Q、K 和 V 矩陣。

一旦我們有了 Q、K 和 V 矩陣,我們就可以使用函數 _attn 來執行注意力。此函數複製了上述注意力點積公式。

class Attention(nn.Module):

def __init__(self, d_model=768, n_head=12, n_ctx=1024, d_head=64, bias=True, scale=False):

super.__init__

self.n_head = n_head

self.d_model = d_model

self.c_attn = Conv1D(d_model, d_model*3)

self.scale = scale

self.softmax = nn.Softmax(dim=-1)

self.register_buffer("bias", torch.tril(torch.ones(n_ctx, n_ctx)).view(1, 1, n_ctx, n_ctx))

self.dropout = nn.Dropout(0.1)

self.c_proj = Conv1D(d_model, d_model)

def split_heads(self, x):

"return shape [`batch`, `head`, `sequence`, `features`]"

new_shape = x.size[:-1] + (self.n_head, x.size(-1)//self.n_head)

x = x.view(*new_shape)

return x.permute(0, 2, 1, 3)

def _attn(self, q, k, v, attn_mask=None):

scores = torch.matmul(q, k.transpose(-2, -1))

if self.scale: scores = scores/math.sqrt(v.size(-1))

nd, ns = scores.size(-2), scores.size(-1)

if attn_mask is not None: scores = scores + attn_mask

scores = self.softmax(scores)

scores = self.dropout(scores)

outputs = torch.matmul(scores, v)

return outputs

def merge_heads(self, x):

x = x.permute(0, 2, 1, 3).contiguous

new_shape = x.size[:-2] + (x.size(-2)*x.size(-1),)

return x.view(*new_shape)

def forward(self, x):

x = self.c_attn(x) #new `x` shape - `[1,3,2304]`

q, k, v = x.split(self.d_model, dim=2)

q, k, v = self.split_heads(q), self.split_heads(k), self.split_heads(v)

out = self._attn(q, k, v)

out = self.merge_heads(out)

out = self.c_proj(out)

return out

另一種實現注意力的方法在本博客底部的附加部分進行了說明。我發現它更直觀,更容易與研究論文進行比較。它利用線性層而不是 CONV1D 將輸入轉換為 Q、K 和 V 矩陣。我們之所以沒有使用它,是因為我們使用了預訓練的權重,從 Hugging Face 轉換為一維層。

多頭注意力

下面一段是從論文「Attention is all you need」上摘取的。

我們發現,使用不同的、學習到的線性映射將查詢、鍵和值分別線性映射到 dk、dk 和 dv 維度更好。然後,在這些查詢、鍵和值的隱射版本中,我們並行地執行注意力函數,生成 dv 維輸出值。這些值被連接起來,然後再次進行映射,得到最終值,如下圖所示:

GPT-2没什么神奇的,PyTorch 就可以复现代码

多頭注意力機制允許模型在不同的位置共同關注來自不同表示子空間的信息。

多頭注意力等式

在這項工作中,我們使用了 h=8 個平行的注意力層,或者說頭。其中,我們使用的都是 dk=dv=dmodel/h=64。由於每個頭的維數減少,總的計算成本與全維度的單頭部注意的計算成本相似。

不要被這個弄糊塗了,本質上,我們所做的就是給 Q,K 和 V 矩陣增加一個維數。也就是說,如果這些矩陣之前的大小是 [1, 4, 768],表示 [bs, seq_len, d_model],則這些矩陣被投影到[bs, n_head, seq_len, d_model//n_head],大小為 [1, 12, 4, 64]。GPT-2 使用 12 個平行頭。我們將 Q,K,V 矩陣分解到 split_heads 函數中。最後,當我們通過應用並行注意力得到一個輸出時,我們將它連接到合併頭中,返回到維度矩陣 [bs,seq_len,d_model]。

代碼中的 GPT-2 模型體系結構

GPT-2没什么神奇的,PyTorch 就可以复现代码

到目前為止,我們已經實現了多頭注意和前饋層。如上圖所示,這兩層構成 transformer 解碼器塊的構建塊。GPT-2 由 12 個 transformer 組組成。

這在 Jay Alammar 的文章中顯示如下:

GPT-2没什么神奇的,PyTorch 就可以复现代码

由 12 個解碼塊組成的 GPT 體系結構

transformer 解碼器塊說明

class TransformerBlock(nn.Module):

def __init__(self, d_model=768, n_head=12, dropout=0.1):

super(TransformerBlock, self).__init__

self.attn = Attention(d_model=768, n_head=12, d_head=64, n_ctx=1024, bias=True, scale=False)

self.feedforward = FeedForward(dropout=0.1, d_model=768, nx=768*4)

self.ln_1 = LayerNorm(d_model)

self.ln_2 = LayerNorm(d_model)

def forward(self, x):

x = x + self.attn(self.ln_1(x))

x = x + self.feedforward(self.ln_2(x))

return x

transformer 組由注意力層和前饋層組成,如 GPT-2 架構模型規範所述:層規範化被移動到每個子塊的輸入,這裡的子塊是注意力和前饋。

因此,在 transformer 解碼器塊中,我們首先將輸入傳遞給一個 LayerNorm,然後是第一個子注意力塊。接下來,我們將這個子塊的輸出再次傳遞給 LayerNorm,最後傳遞給前饋層。

GPT-2架構說明

如 GPT 論文所述:我們訓練了一個 12 層的只解碼的 transformer,它有隱藏的自注意力頭(768 個維度和 12 個注意力頭)。

因此,完整的 GPT-2 體系結構是經過 12 次複製的 TransformerBlock。

def _get_clones(module, n): return ModuleList([copy.deepcopy(module) for i in range(n)])

class GPT2(nn.Module):

def __init__(self, nlayers=12, n_ctx=1024, d_model=768, vcb_sz=50257):

super(GPT2, self).__init__

self.nlayers = nlayers

block = TransformerBlock(d_model=768, n_head=12, dropout=0.1)

self.h = _get_clones(block, 12)

self.wte = nn.Embedding(vcb_sz, d_model)

self.wpe = nn.Embedding(n_ctx, d_model)

self.drop = nn.Dropout(0.1)

self.ln_f = LayerNorm(d_model)

self.out = nn.Linear(d_model, vcb_sz, bias=False)

self.loss_fn = nn.CrossEntropyLoss

self.init_weights

def init_weights(self):

self.out.weight = self.wte.weight

self.apply(self._init_weights)

def _init_weights(self, module):

if isinstance(module, (nn.Linear, nn.Embedding, Conv1D)):

module.weight.data.normal_(mean=0.0, std=0.02)

if isinstance(module, (nn.Linear, Conv1D)) and module.bias is not None:

module.bias.data.zero_

elif isinstance(module, nn.LayerNorm):

module.bias.data.zero_

module.weight.data.fill_(1.0)

def forward(self, src, labels=None, pos_ids=None):

if pos_ids is None: pos_ids = torch.arange(0, src.size(-1)).unsqueeze(0)

inp = self.drop((self.wte(src)+self.wpe(pos_ids)))

for i in range(self.nlayers): inp = self.h[i](inp)

inp = self.ln_f(inp)

logits = self.out(inp)

outputs = (logits,) + (inp,)

if labels is not None:

shift_logits = logits[..., :-1, :].contiguous

shift_labels = labels[..., 1:].contiguous

loss = self.loss_fn(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))

outputs = (loss,) + outputs

return outputs

return logits

我還沒有提到的是位置編碼和標記嵌入。因為,我們不能將諸如「hey」或「hello」之類的詞直接傳遞給模型,所以我們首先將輸入標記化。接下來,我們使用嵌入將標記表示為數字。Jay Alammar 的這篇文章(http://jalammar.github.io/illustrated-word2vec/)很好地解釋了嵌入。

此外,與按順序傳遞輸入詞的 RNN 不同,transformer 並行地接受輸入矩陣,從而失去了被輸入詞的位置感。為了彌補這一損失,在將標記嵌入處理到模型之前,我們添加了 Positional Encoding——一種指示序列中單詞順序的信號。如前所述,由於 GPT-2 的上下文大小是 1024,因此位置編碼的維度是 [1024, 768]。

GPT-2没什么神奇的,PyTorch 就可以复现代码

從[The Illustrated GPT-2]引用的位置編碼(http://jalammar.github.io/Illustrated-gpt2/)

因此,GPT-2 體系結構的輸入是通過一個 Dropout 的標記嵌入和位置編碼的總和。一旦我們有了輸入矩陣,我們就讓其通過 GPT-2 架構的 12 層中的每一層,其中每一層都是一個由兩個子層組成的 transformer 譯碼器塊——注意力和前饋網絡。

語言建模或分類

當使用 GPT-2 作為語言模型時,我們將輸入傳遞到最終層形式,並通過最終大小為[768, vocab_sz](50257)的線性層,得到大小為[1,4,50257]的輸出。這個輸出表示下一個詞彙輸入,我們現在可以很容易地通過一個 softmax 層,並使用 argmax 以最大的概率獲得單詞在詞彙表中的位置。

對於分類任務,我們可以通過大小為 [768, n] 的線性層來傳遞從 GPT-2 架構接收到的輸出,以獲得每個類別的概率(其中 n 表示類別的數量),然後通過 softmax 傳遞,得到最高的預測類別,並使用 CrossEntropyLoss 來訓練架構進行分類。

這就是 GPT-2 背後的全部魔法。它是一種基於解碼器的 transformer 式結構,與 RNN 不同,它採用與位置編碼並行的輸入,通過 12 個 transformer 解碼器層(由多頭注意力和前饋網絡組成)中的每一層來返回最終輸出。

讓我們在語言模型任務中看看這個模型的實際作用。

使用 Hugging Face 預訓練權重生成示例文本

首先,讓我們用 Hugging Face 提供的預訓練權重初始化模型。

model = GPT2

# load pretrained_weights from hugging face

# download file https://s3.amazonaws.com/models.huggingface.co/bert/gpt2-pytorch_model.bin to `.`

model_dict = model.state_dict #currently with random initializationstate_dict = torch.load("./gpt2-pytorch_model.bin") #pretrained weights

old_keys =

new_keys =

for key in state_dict.keys:

if "mlp" in key: #The hugging face state dict references the feedforward network as mlp, need to replace to `feedforward` be able to reuse these weights

new_key = key.replace("mlp", "feedforward")

new_keys.append(new_key)

old_keys.append(key)

for old_key, new_key in zip(old_keys, new_keys): state_dict[new_key]=state_dict.pop(old_key)

pretrained_dict = {k: v for k, v in state_dict.items if k in model_dict}

model_dict.update(pretrained_dict)

model.load_state_dict(model_dict)

model.eval #model in inference mode as it's now initialized with pretrained weights

現在讓我們生成文本。我們將使用 Hugging Face 的預訓練標記器將單詞轉換為輸入嵌入。

from transformers import GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained("gpt2")

context = torch.tensor([tokenizer.encode("The planet earth")])

def generate(context, ntok=20):

for _ in range(ntok):

out = model(context)

logits = out[:, -1, :]

indices_to_remove = logits logits[indices_to_remove] = np.NINF

next_tok = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1).squeeze(1)

context = torch.cat([context, next_tok.unsqueeze(-1)], dim=-1)

return context

out = generate(context, ntok=20)tokenizer.decode(out[0])

>> 'The planet earth is the source of all of all the light," says the study that the government will'

附加內容

另一種實現注意力的方法,在 fast.ai 的 NLP 課程(https://github.com/fastai/course-nlp/blob/master/8-translation-transformer.ipynb)中有,我發現更直觀的方法如下:

class Attention_FASTAI(nn.Module):

def __init__(self, d_model=768, n_head=12, d_head=64, n_ctx=1024, bias=True, scale=False):

super.__init__

self.n_head = n_head

self.d_head = d_head

self.softmax = nn.Softmax(dim=-1)

self.scale = scale

self.atn_drop = nn.Dropout(0.1)

self.wq, self.wk, self.wv = [nn.Linear(d_model, n_head*d_head,

bias=bias) for o in range(3)]

def split_heads(self, x, layer, bs):

x = layer(x)

return x.view(bs, x.size(1), self.n_head, self.d_head).permute(0,2,1,3)

def _attn(self, q, k, v, attn_mask=None):

scores = torch.matmul(q, k.transpose(-2, -1))

if self.scale: scores = scores/math.sqrt(v.size(-1))

if attn_mask is not None:

scores = scores.float.masked_fill(attn_mask, -float('inf')).type_as(scores)

attn_prob = self.atn_drop(self.softmax(scores))

attn_vec = attn_prob @ v

return attn_vec

def merge_heads(self, x, bs, seq_len):

x = x.permute(0, 2, 1, 3).contiguous

return x.view(bs, seq_len, -1)

def forward(self, q, k, v, mask=None):

bs, seq_len = q.size(0), q.size(1)

wq, wk, wv = map(lambda o:self.split_heads(*o, bs),

zip((q,k,v), (self.wq, self.wk, self.wv)))

attn_vec = self._attn(wq, wk, wv)

attn_vec = self.merge_heads(attn_vec, bs, seq_len)

return attn_vec

上面的實現與我們採用的實現方法的關鍵區別在於,這個實現沒有使用 CONV1D,而是先將輸入 x 傳遞給 self.wq、self.wk 和 self.wv 線性層,得到 wq、wk 和 wv 矩陣,然後接下來和前面一樣。

寫在最後

特別感謝 Hugging Face 創建了一個開源的 NLP 庫,並提供了許多可使用的預訓練模型。如前所述,本文中的代碼直接來自 Hugging Face 庫。The Illustrated GPT-2(http://jalammar.github.io/illustrated-gpt2/)是關於 GPT-2 知識最全的博客之一。最後,Harvard NLP 的 The Annotated Transformer(https://nlp.seas.harvard.edu/2018/04/03/attention.html)完成了一個很棒且易於學習的 PyTorch 中 Transformers 的實現。

via:https://amaarora.github.io/2020/02/18/annotatedGPT2.html

雷鋒網雷鋒網雷鋒網


分享到:


相關文章: