翻譯Deep Learning and the Game of Go(14)第十二章 採用actor-critic方法的強化學習

本章包括:

  • 利用優勢使強化學習更有效率
  • 用actor-critic方法來實現自我提升AI
  • 設計和訓練Keras的多輸出神經網絡

如果你正在學習下圍棋,最好的改進方法之一是讓一個水平更高的棋手給你覆盤。有時候,最有用的反饋只是指出你在哪裏贏棋或輸棋。覆盤的人可能會給出這樣的評論,“你下了30步後已經遠遠落後了”或“在下了110步後,你有一個獲勝的局面,但你的對手在130時扭轉了局面。”

爲什麼這種反饋是有幫助的?你可能沒有時間仔細檢查一個對局中的所有300個落子,但你可以把全部注意力集中在10或20個落子序列上。覆盤的人可以讓你知道對局中的哪些部分是重要的。

強化學習研究人員將這一原則應用於actor-critic學習,它是策略學習(如第10章所述)和價值學習(如第11章所述)的結合。策略函數在其中扮演着演員的角色:它選擇要下的落子點。價值函數在其中是批評家:它跟蹤代理在遊戲過程中是領先還是落後。這種反饋指導着訓練過程,就像遊戲覆盤一樣指導你的學習。

本章描述瞭如何用actor-critic學習來做自我提升的遊戲AI。讓其工作的核心概念就是優勢(advantage),時實際遊戲結果和預期結果之間的區別。我們首先要說明怎樣讓優勢去改進訓練過程。在那之後,我們準備構建一個actor-critic遊戲AI。首先,我們將展示如何實現落子選擇,然後我們要實現新的訓練過程。在這兩個函數中,我們大量借鑑了第10章和11章中的代碼示例,最終的結果是最好的:它將策略學習和價值學習的好處結合成一個代理。

12.1 advantage 教你哪個決策是重要的

在第十章中,我們簡要提到了信度分配問題。假設你的學習代理下了一個200步的對局,最終取勝。因爲它贏了,你可以假設它是至少選擇了一些好的落子點,但它可能也選擇了一些不好的落子點。信度分配沒有把你想要強化的好落子和你想要忽視的壞落子分開。本節介紹了優勢的概念,這是一個估計某一特定決策對最終結果貢獻的公式。首先,我們描述了優勢如何幫助信度分配;然後我們提供代碼樣本來說明如何去計算。

12.1.1. 什麼是advantage? 

想象一下,你正在看一場籃球比賽;當第四節快結束的時候,你最喜歡的球員卻投出了一個三分球。你有多興奮是取決於遊戲狀態的。如果分數是80比78,你可能要從座位上跳起來,而如果分數是110比80,你就會無動於衷。這有什麼區別?在一場快結束的比賽中,三分球造成了遊戲預期結果的巨大變化。另一方面,如果遊戲是一種吊打的局面,那一個單獨的動作不會影響任何的結果。當遊戲結果還未定時,這時的動作是最重要的。在強化學習方面,advantage就是一個量化這一概念的公式。

爲了計算advantage,您首先需要對狀態的價值進行估計,我們將其表示爲V(s)。這是代理將要看到的預期回報,給出一個特定狀態s下的已經做到的地方。在遊戲中,你可以認爲V(s)可以指示當前局面對黑方有利還是白方有利。如果V(s)接近於1,就說明當前局面非常有利;如果V(s)接近於-1,你的代理人就會輸棋。

如果你回憶起上一章中的action-value函數Q(s,a),可以知道這個概念是相似的。不同之處在於V(s)表示在你選擇落子之前,棋局是多麼有利;Q(s,a)表示在你選擇落子之後棋局是多麼有利。

優勢的定義通常規定如下:

A=Q(s,a)-V(s)

可以這樣思考這個公式:如果你處於一個好的狀態(也就是說,V(S)是高的),但你下了一個差的落子(也就是說Q(s,a)是低的),此時計算是負的,因此你的優勢就會消失。然而,這個公式的問題是,你不知道如何去計算Q(s,a)。但你可以認爲你在遊戲結束時得到的回報就是對真實Q的無偏估計。所以你可以在得到你的回報R後再估計優勢,公式如下:

A=R-V(S)

這是你將在本章中用來估計優勢的公式。讓我們看看這個值是怎樣用的。

爲了說明目的,你會假裝你已經有了一個準確的方法來估計V(s)。實際上,你的代理人會同時學習價值估計函數和策略函數。下一節將要介紹這是如何運作的。

讓我們來舉幾個例子:

  • 在遊戲開始時,V(s)=0:兩個棋手有着近乎相同的機會。假設你的代理贏了比賽,那麼它的回報將是1,所以它第一次移動的優勢是1-0=1。
  • 想象一下游戲快結束了,你的代理幾乎要鎖定勝局了,假定V(S)=0.95。如果你的代理贏得對局,那麼優勢就是1-0.95=0.05。 
  • 現在想象一下你的代理有另一個獲勝的局面,其中再次出現V(S)=0.95。但在這個遊戲中,你的AI不知何故輸掉了對局,給了它一個回報-1,那麼這時advantage就是-1-0.95=-1.95

圖12.1和12.2說明了假想遊戲的優勢計算。在這個遊戲中,你的學習代理在前幾步慢慢領先;然後它犯了一些大錯誤,然後一路落後直到快要輸棋。在150步落子之前的某個地方,它突然設法逆轉了比賽,最終獲得了勝利。在第10章的策略梯度技術下,遊戲中每一步的權重都是相等的。在actor-critic學習中,你想找到最重要的落子並給予它們更大的權重。

 圖12.1 假定遊戲過程中的估計價值。這場比賽持續了200步。起初,學習AI稍微領先,然後它遠遠落後,然後它突然停止落後逆轉了比賽,取得了勝利

由於學習AI獲勝,利用A(s)=1-V(s)公式可以計算出優勢。在圖12.2中,您可以看到優勢曲線與估值曲線具有相同的形狀,但是做了翻轉。最大的優勢來自於代理人遠遠落後的時候,因爲大多數棋手在如此糟糕的情況下都會輸,所以AI一定在某個地方做了一個很好的落子。 

圖12.2  一個假設遊戲中每個落子的優勢。學習代理贏了這場比賽,所以它的最後回報是1,導致恢復局勢的落子有接近2的優勢,所以他們會在訓練期間得到有力的加強。在比賽結束時,當比賽結果已經決定時,他們的優勢就接近0,所以他們在訓練中幾乎會被忽略。 

在代理已經扭轉局面到先前時,大約下了160步左右,它的決策已不再有趣:遊戲已經結束了,這時的優勢就接近於0。

本章稍後,我們就展示瞭如何根據優勢調整訓練過程。在此之前,你需要通過你的自我對弈過程來計算和存儲優勢。

12.1.2. 在自我對弈中計算優勢 

爲了計算優勢,您將更新您在第9章中定義的ExperienceCollector。最初,一個ExperienceBuffer跟蹤三個並行數組:states、actions和rewards。你可以添加第四個並行數組來跟蹤優勢。要填充此數組,您需要每個狀態下的估計值和最終遊戲結果。直到遊戲結束你纔會有後者,所以在中間的時候中,你可以積累估計值,當遊戲結束後,你可以把它們轉化爲優勢。 

    def __init__(self):
        self.states = []
        self.actions = []
        self.rewards = []
        self.advantages = []
        self.current_episode_states = []
        self.current_episode_actions = []
        self.current_episode_estimated_values = [] # 存當前局的所有估值

同樣,您需要更新record_decision方法以接受估計值、狀態和操作。

    # 記錄當前的決定和行動
    def record_decision(self, state, action, estimated_value):
        self.current_episode_states.append(state)
        self.current_episode_actions.append(action)
        self.current_episode_estimated_values.append(estimated_value)

 然後,在complete_episode方法中,您就可以計算代理所做的每個決策的優勢。

    def complete_episode(self, reward):
        num_states = len(self.current_episode_states)
        # +=用在列表表示連接列表
        self.states += self.current_episode_states
        self.actions += self.current_episode_actions
        # 給遊戲中從episode開始到結束時的每一個動作都分配最後的回報(贏是1,輸是-1)
        self.rewards += [reward for _ in range(num_states)]

        #計算優勢,利用R-V(s)
        for i in range(num_states):
            advantage = reward-self.current_episode_estimated_values[i]
            self.advantages.append(advantage)
            
        #一局結束後重置
        self.current_episode_states = []
        self.current_episode_actions = []
        self.current_episode_estimated_values = []

 您還需要更新ExperienceBuffer類和combine_Experience來處理這些優點。

class ExperienceBuffer:

    def __init__(self, states, actions, rewards,advantages):
        self.states = states
        self.actions = actions
        self.rewards = rewards
        self.advantages = advantages

    # 序列化經驗文件
    def serialize(self, h5file):
        h5file.create_group('experienceData')
        h5file['experienceData'].create_dataset('states', data=self.states)
        h5file['experienceData'].create_dataset('actions', data=self.actions)
        h5file['experienceData'].create_dataset('rewards', data=self.rewards)
        h5file['experienceData'].create_dataset('advantages', data=self.advantages)

def combine_experience(collectors):
    combined_states = np.concatenate([np.array(c.states) for c in collectors])
    combined_actions = np.concatenate([np.array(c.actions) for c in collectors])
    combined_rewards = np.concatenate([np.array(c.rewards) for c in collectors])
    combined_advantages = np.concatenate([np.array(c.advantages) for c in collectors])

    return ExperienceBuffer(
        combined_states,
        combined_actions,
        combined_rewards,
        combined_advantages
     )


# 加載經驗文件
def load_experience(h5file):
    return ExperienceBuffer(
        states=np.array(h5file['experienceData']['states']),
        actions=np.array(h5file['experienceData']['actions']),
        rewards=np.array(h5file['experienceData']['rewards']),
        advantages=np.array(h5file['experienceData']['advantages'])
    )

您的experience類現在已經準備好跟蹤優勢。您仍然可以不依賴優勢去使用這些類;在訓練時只需忽略優勢緩衝區的內容即可

12.2.設計一種actor-critic學習神經網絡 

第11章介紹瞭如何在Keras中定義一個具有雙輸入的神經網絡。這個Q-learning網絡一個輸入是棋盤,一個輸入是建議落子。對於actor-critic來說,你想要學習一個輸入和兩個輸出的網絡。這個輸入是棋盤狀態的表示。一個輸出是落子的概率分佈,另一個輸出表示當前局面的預測返回---點評。

建立一個具有兩個輸出的網絡會帶來一個令人驚訝的好處:每個輸出在另一個輸出上充當一種規範化。(回顧第6章,規範化技術是防止訓練時您的模型過度擬合的確切數據集)。想象一下,棋盤上的一串棋子有被吃掉的危險。這個事實與價值輸出有關,因爲擁有弱棋子的玩家可能落後了。它也與落子輸出相關,因爲你可能想要攻擊或防禦薄弱的棋子。如果您的網絡在早期層學習“弱棋子”檢測器,那就會與兩個輸出都相關。對這兩個輸出訓練迫使網絡學習一個對兩個目標都有用的表示。這往往可以提高泛化能力,有時甚至可以加快訓練速度。

第11章介紹了Keras函數API,它使您有充分的自由在您的網絡中連接層。您將在這裏再次使用它來構建圖12.3中描述的網絡;此代碼將在init_ac_agent.py腳本中進行。

from keras.models import Model
from keras.layers import Conv2D,Flatten,Dense,ZeroPadding2D,Input
from dlgo.Encoder.ElevenPlaneEncoder import ElevenPlaneEncoder
from dlgo.agent.ReinforcementLearning.ACAgent import ACAgent


board_size = 19
encoder = ElevenPlaneEncoder(board_size)
board_input = Input(encoder.shape(),name="board_input")
action_input = Input(encoder.num_points(),name="action_input")

conv1 = Conv2D(64, (3, 3), padding='same', activation='relu')(board_input)
conv2 = Conv2D(64, (3, 3), padding='same', activation='relu')(conv1)
conv3 = Conv2D(64, (3, 3), padding='same', activation='relu')(conv2)

# 此示例使用大小爲512的隱藏層。實驗尋找最佳尺寸,這三個隱藏層不需要是相同的大小。
flat= Flatten()(conv3)
processed_board = Dense(512)(flat)

# 策略輸出
policy_hidden_layer = Dense(
    512, activation='relu')(processed_board)
policy_output = Dense(
    encoder.num_points(), activation='softmax')(
    policy_hidden_layer)

# 價值輸出
value_hidden_layer = Dense(
    512, activation='relu')(
    processed_board)
value_output = Dense(1, activation='tanh')(
    value_hidden_layer)

model = Model(inputs=board_input,outputs=[policy_output,value_output])

agent = ACAgent(model,encoder)
initial_agent_filename = "initial_acAgent.h5"

with open(initial_agent_filename,"w") as out_file:
    agent.serialize(out_file)

 

圖12.3  適合actor-critic學習的神經網絡。這個網絡有一個單一的輸入,它表示當前的棋局。網絡產生兩個輸出:一個輸出指示哪些棋子應該下,這是策略輸出,或者叫演員。另一個輸出指示哪個棋手在遊戲中處於領先,這是價值輸出,或者稱爲評論家。評論家不用於下棋,但可以在訓練過程起到幫助。

 這個網絡有三個卷積層,每個有64個過濾器。這是針對一個較小尺寸的棋盤,但有優勢。一如既往,我們鼓勵你嘗試不同的網絡結構。

策略輸出表示可能落子的概率分佈。維度等於棋盤上的交叉點數,您可以使用softmax激活函數,以確保策略總和爲1。

價值輸出是-1到1範圍內的一個數字。此輸出具有維度1,您可以使用tanh激活函數來限制該值的範圍。

12.3 和一個actor-critic代理進行對局 

選擇落子幾乎與第10章中的策略代理完全相同。你做了兩個改變。首先,由於模型現在產生兩個輸出,所以需要一些額外的代碼來解壓縮結果。第二,您需要將估計價值傳遞給經驗收集器,當然還有狀態和行動。從概率分佈中選擇落子的過程是相同的。下面就展示了更新的select_move實現。我們已經指出了它與第10章策略代理的實施不同的地方。 

 def select_move(self, game_state):
        num_moves = self.encoder.board_width * self.encoder.board_height
        board_tensor = self.encoder.encode(game_state)
        input_tensor = np.array([board_tensor])


        # 因爲這是一個雙輸出模型,預測返回一個包含兩個NumPy矩陣的元組。
        actions,values = self.model.predict(input_tensor)
        # 預測是一個批量調用,可以同時處理幾個棋盤,因此您必須選擇矩陣的第一個元素來獲得您想要的概率分佈。
        move_probs = actions[0]
        # 價值被表示爲一維向量,因此必須拉出第一個元素以獲得作爲普通浮點數
        estimated_value = values[0][0]




        # 裁剪概率分佈
        move_probs = self.clip_probs(move_probs)

        # 把概率轉成一個排序列表(0-360)
        candidates = np.arange(num_moves)
        # 按照落子概率進行採樣,不允許取相同的落子
        ranked_moves = np.random.choice(
            candidates, num_moves, replace=False, p=move_probs)
        # 從最高層開始,找到一個有效的落子,不會減少視野空間
        for point_index in ranked_moves:
            point = self.encoder.decode_point_index(point_index)
            if game_state.is_valid(goboard.Move.play(point)) and \
                    not is_point_true_eye(game_state.board, point, game_state.current_player):
                # 當它選擇到了合法落子時,將決定通知給收集器,多了估計值
                if self.collector is not None:
                    self.collector.record_decision(
                        state=board_tensor,
                        action=point_index,
                        estimated_value = estimated_value
                    )
                return goboard.Move.play(point)
        return goboard.Move.pass_turn()

12.4 用經驗數據訓練actor-critic代理

訓練你的actor-critic網絡看起來就像是第十章中訓練策略網絡和第十一章中訓練actor-value網絡的組合。爲了訓練一個雙輸出網絡,你爲每個輸出分別設立了訓練目標,併爲每個輸出分別選擇了一個損失函數。本節就介紹如何將經驗數據轉換爲訓練目標,以及如何使用Keras的FIT函數用於多輸出。

回想一下如何編碼策略梯度學習的訓練數據。對於任何的遊戲局面,訓練目標是一個與棋盤相同大小的向量,-1或1對應於選擇的落子;1表示贏,-1表示輸。在你的actor-critic學習中,你將使用對訓練數據使用相同的編碼方案,但是你1或-1替換爲棋子的優勢。優勢將具有與最終回報相同的符號,因此遊戲決策的概率將向與簡單策略學習相同的方向移動。但它會更進一步去做那些被認爲很重要的行動,而對於那些優勢接近於零的行動只需稍微移動一點

對於價值輸出,訓練目標是總體的回報。這看起來完全像是Q-learning的訓練目標。圖12.4說明了訓練設置。

圖12.4  actor-critic學習的訓練設置。神經網絡有兩個輸出:一個用於策略,一個用於價值。每個人都有自己的訓練目標。策略產出通過一個與棋盤相同大小的向量訓練。向量單元格中與所選落子點對應的地方被填充爲該落子點計算的優勢;其餘爲零。價值輸出是反映比賽的最終結果。

 當您在一個網絡中有多個輸出時,您可以爲每個輸出選擇不同的損失函數。策略輸出將使用分類交叉熵損失函數,價值輸出採用均方誤差損失函數(請參閱第10章和第11章,解釋爲何這些損失函數對這些目標有意義)。

你將要使用的一個新的Keras特徵是損失權重。默認情況下,Keras會將把每個輸出的損失函數加起來得到整體的損失函數。如果您指定損失權重,Keras將在求和之前會縮放每個單獨的損失函數,這樣可以允許你調整每個輸出的相關性。在我們的實驗中,我們發現與策略損失相比,價值損失比較多,所以我們將價值損失縮小了一半。根據你的確切網絡和訓練數據,你可能需要稍微調整一下損失權重。

每當你調用fit時,Keras都會打印出計算的損失值。對於雙輸出網絡,它將分別打印出這兩個損失。你可以查看大小是否可比。如果一個損失遠大於另一個損失,請考慮調整權重,別擔心變得太精確。

下面的列表顯示瞭如何將經驗數據編碼爲訓練數據,然後調用fit在這個訓練目標上。該結構類似於第10章和第11章中的訓練實現。 

既然你已經擁有了所有的片段,讓我們端到端地嘗試actor-critic學習。你將從一個9×9機器人開始,這樣你就可以快速地看到結果。循環就會這樣進行:

  1. 在遊戲中生成自我對弈遊戲塊5000局。
  2. 在每局之後,訓練代理並將其與之前版本的AI進行比較。
  3. 如果新的AI能在100場比賽中擊敗之前的機器人60場,那麼你就已經成功地改進了你的代理。請開始這個過程
  4. 如果新的AI不能在100場比賽中擊敗之前的機器人60場,那麼請生成新的自我對弈數據,並再次訓練。持續訓練直到新的AI足夠強大

在100場中,更新的AI獲得60場獲勝的基準有點武斷;但這是一個很好的輪數,讓你有合理的信心任務你的機器人真的更強大,而不僅僅是幸運。

請開始使用init_ac_agent腳本來初始化機器人(如清單12.5所示): 

 

from dlgo.agent.FastRandomAgent.goboard_fast import GameState, Player
from dlgo.agent.ReinforcementLearning.ExperienceCollector import ExperienceCollector
from dlgo.utils import print_board
from dlgo.ComputeWinner import compute_game_result
from dlgo.agent.ReinforcementLearning.ExperienceCollector import combine_experience
from collections import namedtuple
from dlgo.agent.ReinforcementLearning.ACAgent.ACAgent import load_policy_agent
import h5py

class GameRecord(namedtuple('GameRecord', 'moves winner margin')):
    pass


def simulate_game(black_player, white_player,board_size):
    moves = []
    game = GameState.new_game(board_size)
    agents = {
        Player.black: black_player,
        Player.white: white_player,
    }
    while not game.is_over():
        next_move = agents[game.current_player].select_move(game)
        moves.append(next_move)
        game = game.apply_move(next_move)

    print_board(game.board)
    game_result = compute_game_result(game)
    print(game_result)

    return GameRecord(
        moves=moves,
        winner=game_result.winner,
        margin=game_result.winning_margin,
    )


def main():
    board_size = 9
    num_games = 5000
    experience_filename = "exp_9_0001.h5"

    agent1 = load_policy_agent(h5py.File("initial_acAgent_9.h5"))
    agent2 = load_policy_agent(h5py.File("initial_acAgent_9.h5"))
    collector1 = ExperienceCollector()
    collector2 = ExperienceCollector()
    agent1.set_collector(collector1)
    agent2.set_collector(collector2)

    # 開始模擬對局
    for i in range(num_games):
        print("開始模擬對局%d/%d" %(i+1,num_games))
        collector1.begin_episode()
        collector2.begin_episode()

        game_record = simulate_game(agent1,agent2,board_size)
        # 黑棋贏了給第一個AI回報1,給第二個AI回報-1
        if game_record.winner == Player.black:
            collector1.complete_episode(reward=1)
            collector2.complete_episode(reward=-1)
        # 白棋贏了給第一個AI回報-1,給第二個AI回報1
        else:
            collector1.complete_episode(reward=-1)
            collector2.complete_episode(reward=1)

    experience = combine_experience([collector1, collector2])
    with h5py.File(experience_filename, 'w') as experience_outf:
            experience.serialize(experience_outf)

if __name__ == '__main__':
    main()

在此之後,您應該有一個exp_0001.hdf5文件,其中包含一大塊遊戲記錄。下一步是訓練:

這將使用當前存儲在ac_v1.hdf1中的神經網絡,對exp_0001.hdf中的數據運行一個單一的訓練,並將更新的代理保存到ac_v2.hdf5中。優化器將使用0.01的學習率,1024的訓練集大小。你應該看到輸出這樣的東西: 

Epoch 1/1 574234/574234 [==============================] ­ 15s 26us/step ­ loss: 1.0277 ­ dense_3_loss: 0.6403 ­ dense_5_loss: 0.7750

 請注意,損失現在分爲兩個值:dense_3_loss和dense_5_loss,分別對應於策略輸出和價值輸出。

與早期版本對局100局,結果如下

... Simulating game 100/100... 
9 oooxxxxx. 
8 .oox.xxxx
7 ooxxxxxxx
6 .oxx.xxxx 
5 oooxxx.xx
4 o.ox.xx.x
3 ooxxxxxxx
2 ooxx.xxxx
1 oxxxxxxx.
 ABCDEFGHJ
B+31.5
Agent 1 record: 60/100

在這種情況下,輸出顯示您準確地實現了100場中獲得60場勝利的閾值:您可以有合理的信心相信您的機器人已經學到了一些有用的東西。(這只是一個例子,與你的實際結果會有點不同,這很好。因爲ac_v2bot比ac_v1強得多,所以您可以使用ac_v2去生成遊戲,然後再次進行訓練和評估,這次情況不像之前那麼成功。

Agent 1 record: 51/100

ac_v3機器人在100次中只擊敗了ac_v2機器人51次。有了這些結果,很難說ac_v3是否更強一點;最保險的結論是,它與ac_v2水平差不多,但不要絕望。您可以生成更多的訓練數據,然後再試一次:

每增加一批遊戲數據後,您可以再次與ac_v2進行對局。在我們的實驗中,我們需要三批5000局對局-總共15000個對局-然後才能得到令人滿意的結果。

Agent 1 record: 62/100

成功!戰勝ac_v262場,現在你可以說ac_v3比ac_v2強。然後,你就可以切換到ac_v3去生成自我對弈遊戲,然後再次重複這個循環過程。

雖然我們不是很清楚一個圍棋機器人可以通過actor-critic得到多強的能力,但我們我們已經證明,你可以訓練一個機器人來學習基本的戰術,但它的力量一定會在某一時刻達到頂峯。通過將強化學習與樹搜索進行深度整合,你可以訓練一個AI讓它比任何人類棋手更強,14章就展示了這個技術。

12.5 總結

  • actor-critic學習是一種強化學習技術,在這種技術中,你可以同時學習策略函數和價值函數。策略函數告訴你應該如何做決定,以及價值函數有助於改進訓練過程。你可以將actor-critic學習應用於與策略梯度學習同樣類型的問題,但是actor-critic學習通常更穩定。
  • 優勢是一個代理人看到的實際回報和預期回報之間的差別。對於遊戲來說,這就是遊戲實際結果(輸或贏)和期望值(由代理的價值模型估計)的差別。
  • 優勢有助於識別遊戲中的重要決策。如果一個學習代理人贏了一場比賽,那麼當比賽快輸的時候,所做的優勢是最大的,而當比賽已經決定後,所做的動作優勢的最小。
  • 一個Keras順序網絡可以有多個輸出。在actor-critic學習中,這可以讓您創建一個單一的網絡來建模策略函數和價值函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章