主要内容

运行一个自定义的图像比较训练实验

这个例子展示了如何创建一个自定义训练实验来训练一个可以识别类似手写字符图像的暹罗网络。对于自定义训练实验,显式地定义所使用的训练过程实验管理器.在这个例子中,你实现了一个自定义训练循环来训练一个Siamese网络,这是一种深度学习网络,它使用两个或多个具有相同架构并共享相同参数和权重的相同子网络。Siamese网络的一些常见应用包括面部识别、签名验证和转述识别。

该图说明了本例中的暹罗网络体系结构。

为了比较两个图像,您将每个图像通过两个共享权重的相同子网中的一个传递。子网络将每张105 × 105 × 1的图像转换为4096维的特征向量。同一类的图像具有类似的4096维表示。每个子网络的输出特征向量通过减法组合,结果通过a传递fullyconnect具有单个输出的操作。一个sigmoid操作将这个值转换为一个概率,表明图像是相似的(当概率接近1时)或不相似的(当概率接近0时)。网络预测和真实标签之间的二进制交叉熵损失在训练期间更新网络。有关更多信息,请参见训练暹罗网络来比较图像

开放实验

首先,打开示例。实验管理器加载一个带有预先配置的实验的项目,您可以检查和运行该实验。打开实验,在实验的浏览器窗格中,双击实验的名称(ImageComparisonExperiment).

自定义训练实验由描述、超参数表和训练函数组成。有关更多信息,请参见配置自定义训练实验

描述字段包含实验的文本描述。对于本例,描述如下:

训练暹罗网络识别手写字符的相似和不同图像。为网络中的卷积层和全连接层尝试不同的权重和偏差初始化器。

Hyperparameters部分指定策略(详尽的扫描)和用于实验的超参数值。运行实验时,“实验管理器”将使用超参数表中指定的超参数值的每个组合来训练网络。本例使用超参数WeightsInitializer而且BiasInitializer为每个子网络中的卷积层和全连接层分别指定权重和偏置初始化器。有关这些初始化式的更多信息,请参见WeightsInitializer而且BiasInitializer

培训功能指定实验使用的训练数据、网络架构、训练选项和训练程序。训练函数的输入是一个结构,其中包含来自超参数表的字段和实验。监控对象,您可以使用该对象跟踪训练的进度、记录训练使用的度量标准的值,并生成训练图。训练函数返回一个结构,其中包含训练后的网络和最终的权重fullyconnect网络操作,以及用于培训的执行环境。实验管理器保存此输出,因此您可以在训练完成后将其导出到MATLAB工作区。训练函数有五个部分。

  • 初始化输出设置网络和的初始值fullyconnect将权重赋给空数组,以指示训练尚未开始。实验设置执行环境为“汽车”,所以它会在GPU上训练和验证网络(如果GPU可用的话)。使用GPU需要并行计算工具箱™和受支持的GPU设备。金宝app有关更多信息,请参见GPU支金宝app持版本(并行计算工具箱)

Output.network = [];输出。权重= [];输出。executionEnvironment =“汽车”
  • 加载和预处理训练和测试数据定义实验的训练和测试数据为imageDatastore对象。实验使用Omniglot数据集,该数据集由50个字母的字符集组成,分为30组用于训练,20组用于测试。有关此数据集的更多信息,请参见图像数据集

班长。状态=“加载训练数据”
url =“https://github.com/brendenlake/omniglot/raw/master/python/images_background.zip”;downloadFolder = tempdir;文件名= fullfile(下载文件夹,“images_background.zip”);
dataFolderTrain = fullfile(下载文件夹,“images_background”);如果~存在(dataFolderTrain“dir”) websave(文件名,url);解压缩(文件名,downloadFolder);结束
imdsTrain = imageDatastore(dataFolderTrain,...IncludeSubfolders = true,...LabelSource =“没有”);
files = imdsTrain.Files;Parts = split(files,filesep);标签= join(parts(:,(end-2):(end-1)),“_”);imdsTrain。标签=分类的(标签);
班长。状态=“加载测试数据”
url =“https://github.com/brendenlake/omniglot/raw/master/python/images_evaluation.zip”;downloadFolder = tempdir;文件名= fullfile(下载文件夹,“images_evaluation.zip”);
dataFolderTest = fullfile(下载文件夹,“images_evaluation”);如果~存在(dataFolderTest“dir”) websave(文件名,url);解压缩(文件名,downloadFolder);结束
imdsTest = imageDatastore(dataFolderTest,...IncludeSubfolders = true,...LabelSource =“没有”);
files = imdsTest.Files;Parts = split(files,filesep);标签= join(parts(:,(end-2):(end-1)),“_”);imdsTest。标签=分类的(标签);
  • 定义网络架构定义两个相同的子网的架构,接受105 × 105 × 1图像并输出特征向量。卷积层和全连接层使用超参数表中指定的权重和偏置初始化器。为了使用自定义训练循环训练网络并启用自动区分,训练函数将层图转换为dlnetwork对象。决赛的重量fullyconnect操作初始化是通过从标准偏差为0.01的窄正态分布中随机抽样。

班长。状态=“创建网络”
layers = [imageInputLayer([105 105 1],Name=“input1”归一化=“没有”) convolution2dLayer (Name = 64“conv1”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu1”) maxPooling2dLayer(= 2步= 2的名字“maxpool1”) convolution2dLayer(7128年,Name =“conv2”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu2”) maxPooling2dLayer (2“步”2 Name =“maxpool2”) convolution2dLayer (4128,“名字”“conv3”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu3”) maxPooling2dLayer (2“步”2 Name =“maxpool3”) convolution2dLayer(5256年,Name =“conv4”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu4”) fullyConnectedLayer(4096年,Name =“fc1”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer)];
lgraph = layerGraph(图层);Dlnet = dlnetwork(lgraph);
fcWeights = dlarray(0.01*randn(1,4096));fcBias = dlarray(0.01*randn(1,1));fcParams = struct(...“FcWeights”fcWeights,...“FcBias”, fcBias);
Output.network = dlnet;输出。weights = fcParams;
  • 指定培训项目定义实验使用的训练选项。在这个例子中,实验管理器用180的小批大小训练网络进行1000次迭代,每100次迭代计算一次网络的准确性。跑步训练需要一些时间。为了获得更好的结果,可以考虑将训练次数增加到10,000次。

numIterations = 1000;miniBatchSize = 180;validationFrequency = 100;initialLearnRate = 6e-5;gradientDecayFactor = 0.9;squaredGradientDecayFactor = 0.99;trailingAvgSubnet = [];trailingAvgSqSubnet = [];trailingAvgParams = [];trailingAvgSqParams = [];
  • 火车模型定义实验使用的自定义训练循环。对于每次迭代,自定义训练循环提取一批图像对和标签,将数据转换为dlarray对象,并指定维度标签“SSCB”(空间,空间,通道,批)为图像数据和“CB”(通道,批次)标签。如果在GPU上训练,则数据将转换为gpuArray(并行计算工具箱)对象。然后,训练函数评估模型梯度并更新网络参数。为了验证,训练函数创建了一组5个随机小批量测试对,评估网络预测,并计算小批量的平均精度。在自定义训练循环的每次迭代之后,训练函数保存训练的网络和权值fullyconnect操作,记录训练损失,更新训练进度。

班长。度量= [“TrainingLoss”“ValidationAccuracy”];班长。包含=“迭代”;班长。状态=“培训”
iteration = 1:numIterations [X1,X2,pairLabels] = getSiameseBatch(imdsTrain,miniBatchSize);dlX1 = dlarray(single(X1),“SSCB”);dlX2 = dlarray(single(X2),“SSCB”);如果(输出。executionEnvironment = =“汽车”||...输出。executionEnvironment = =“图形”dlX1 = gpuArray(dlX1);dlX2 = gpuArray(dlX2);结束[gradientsSubnet, gradientsParams,loss] = dlfeval(@modelGradients,...dlnet、fcParams dlX1、dlX2 pairLabels);lossValue = double(gather(extractdata(loss)));[dlnet, trailingAvgSubnet trailingAvgSqSubnet] =...adamupdate (dlnet gradientsSubnet,...trailingAvgSubnet trailingAvgSqSubnet,...迭代,initialLearnRate、gradientDecayFactor squaredGradientDecayFactor);[fcParams, trailingAvgParams trailingAvgSqParams] =...adamupdate (fcParams gradientsParams,...trailingAvgParams trailingAvgSqParams,...迭代,initialLearnRate、gradientDecayFactor squaredGradientDecayFactor);如果~rem(iteration,validationFrequency) || iteration == 1 || iteration == numIterations监视器。状态=“验证”;精度= 0 (1,5);精度= 150;i = 1:5 [XAcc1,XAcc2,pairLabelsAcc] = getSiameseBatch(imdsTest,accuracyBatchSize);dlXAcc1 = dlarray(single(XAcc1),“SSCB”);dlXAcc2 = dlarray(single(XAcc2),“SSCB”);如果(输出。executionEnvironment = =“汽车”||...输出。executionEnvironment = =“图形”dlXAcc1 = gpuArray(dlXAcc1);dlXAcc2 = gpuArray(dlXAcc2);结束dlY = predictSiamese(dlnet,fcParams,dlXAcc1,dlXAcc2);Y = gather(extractdata(dlY));Y =圆(Y);precision (i) = sum(Y == pairLabelsAcc)/accuracyBatchSize;结束recordMetrics(监控、迭代...ValidationAccuracy =意味着(精度)* 100);班长。状态=“培训”结束Output.network = dlnet;输出。weights = fcParams;recordMetrics(监控、迭代...TrainingLoss = lossValue);班长。进度=(迭代/numIterations)*100;如果班长。停止返回结束结束

检查训练功能,下培训功能,点击编辑.训练功能在MATLAB®编辑器中打开。此外,训练函数的代码出现在附录1在这个例子的最后。

运行实验

当您运行实验时,实验管理器将多次训练由训练函数定义的网络。每次试验使用不同的超参数值组合。默认情况下,实验管理器每次运行一个试验。如果您有并行计算工具箱,您可以同时运行多个试验。为了获得最好的结果,在运行实验之前,启动一个具有与gpu一样多的worker的并行池。有关更多信息,请参见使用实验管理器并行训练网络

  • 若要一次运行一个试验,请在“实验管理器”工具条上单击运行

  • 若要同时运行多个试验,请单击使用并行然后运行.如果当前没有并行池,experimental Manager将使用默认集群配置文件启动一个并行池。然后,根据可用的并行工作人员的数量,实验管理器执行多个同时进行的试验。

结果表显示了每个试验的训练损失和验证精度。

当实验正在运行时,单击培训策划展示训练图,跟踪每次试验的进度。

评估结果

要为您的实验找到最佳结果,请根据验证精度对结果表进行排序。

  1. 指向ValidationAccuracy列。

  2. 单击三角形图标。

  3. 选择按降序排序

具有最高验证准确性的试验显示在结果表的顶部。

为了直观地检查网络是否正确地识别相似和不相似的对:

  1. 选择准确率最高的试验。

  2. 实验管理器将来发布,点击出口

  3. 在对话框窗口中,为导出的训练输出输入工作区变量的名称。默认名称为trainingOutput

  4. 方法在一小批图像对上测试网络displayTestSet函数中列出的附录3在这个例子的最后。使用导出的训练输出作为函数的输入。例如,在MATLAB命令窗口中输入:

displayTestSet (trainingOutput)

该函数显示10个随机选择的测试图像对,带有来自训练网络的预测、概率分数和指示预测是否正确的标签。

要记录对实验结果的观察,请添加注释。

  1. 在结果表中,右键单击ValidationAccuracy最好的审判细胞。

  2. 选择添加注释

  3. 注释窗格,在文本框中输入您的观察结果。

有关更多信息,请参见对实验结果进行排序、过滤和注释

关闭实验

实验的浏览器窗格中,右键单击项目的名称并选择关闭项目.实验管理器关闭项目中包含的所有实验和结果。

附录1:培训功能

这个函数指定了实验使用的训练数据、网络架构、训练选项和训练过程。

输入

  • 参数个数是一个包含来自experimental Manager超参数表字段的结构。

  • 监控是一个实验。监控对象,您可以使用该对象跟踪训练的进度、更新结果表中的信息字段、记录训练使用的度量值以及生成训练图。

输出

  • 输出一个结构是否包含训练过的网络,最终的权重fullyconnect网络操作,以及用于培训的执行环境。实验管理器保存此输出,因此您可以在训练完成后将其导出到MATLAB工作区。

函数output = ImageComparisonExperiment_training1(params,monitor) output.network = [];输出。权重= [];输出。executionEnvironment =“汽车”;班长。状态=“加载训练数据”;url =“https://github.com/brendenlake/omniglot/raw/master/python/images_background.zip”;downloadFolder = tempdir;文件名= fullfile(下载文件夹,“images_background.zip”);dataFolderTrain = fullfile(下载文件夹,“images_background”);如果~存在(dataFolderTrain“dir”) websave(文件名,url);解压缩(文件名,downloadFolder);结束imdsTrain = imageDatastore(dataFolderTrain,...IncludeSubfolders = true,...LabelSource =“没有”);files = imdsTrain.Files;Parts = split(files,filesep);标签= join(parts(:,(end-2):(end-1)),“_”);imdsTrain。标签=分类的(标签);班长。状态=“加载测试数据”;url =“https://github.com/brendenlake/omniglot/raw/master/python/images_evaluation.zip”;downloadFolder = tempdir;文件名= fullfile(下载文件夹,“images_evaluation.zip”);dataFolderTest = fullfile(下载文件夹,“images_evaluation”);如果~存在(dataFolderTest“dir”) websave(文件名,url);解压缩(文件名,downloadFolder);结束imdsTest = imageDatastore(dataFolderTest,...IncludeSubfolders = true,...LabelSource =“没有”);files = imdsTest.Files;Parts = split(files,filesep);标签= join(parts(:,(end-2):(end-1)),“_”);imdsTest。标签=分类的(标签);班长。状态=“创建网络”;layers = [imageInputLayer([105 105 1],Name=“input1”归一化=“没有”) convolution2dLayer (Name = 64“conv1”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu1”) maxPooling2dLayer(= 2步= 2的名字“maxpool1”) convolution2dLayer(7128年,Name =“conv2”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu2”) maxPooling2dLayer (2“步”2 Name =“maxpool2”) convolution2dLayer (4128,“名字”“conv3”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu3”) maxPooling2dLayer (2“步”2 Name =“maxpool3”) convolution2dLayer(5256年,Name =“conv4”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer) reluLayer (Name =“relu4”) fullyConnectedLayer(4096年,Name =“fc1”...WeightsInitializer =参数。WeightsInitializer,...BiasInitializer = params.BiasInitializer)];lgraph = layerGraph(图层);Dlnet = dlnetwork(lgraph);fcWeights = dlarray(0.01*randn(1,4096));fcBias = dlarray(0.01*randn(1,1));fcParams = struct(...“FcWeights”fcWeights,...“FcBias”, fcBias);Output.network = dlnet;输出。weights = fcParams;numIterations = 1000;miniBatchSize = 180;validationFrequency = 100;initialLearnRate = 6e-5;gradientDecayFactor = 0.9;squaredGradientDecayFactor = 0.99; trailingAvgSubnet = []; trailingAvgSqSubnet = []; trailingAvgParams = []; trailingAvgSqParams = []; monitor.Metrics = [“TrainingLoss”“ValidationAccuracy”];班长。包含=“迭代”;班长。状态=“培训”iteration = 1:numIterations [X1,X2,pairLabels] = getSiameseBatch(imdsTrain,miniBatchSize);dlX1 = dlarray(single(X1),“SSCB”);dlX2 = dlarray(single(X2),“SSCB”);如果(输出。executionEnvironment = =“汽车”||...输出。executionEnvironment = =“图形”dlX1 = gpuArray(dlX1);dlX2 = gpuArray(dlX2);结束[gradientsSubnet, gradientsParams,loss] = dlfeval(@modelGradients,...dlnet、fcParams dlX1、dlX2 pairLabels);lossValue = double(gather(extractdata(loss)));[dlnet, trailingAvgSubnet trailingAvgSqSubnet] =...adamupdate (dlnet gradientsSubnet,...trailingAvgSubnet trailingAvgSqSubnet,...迭代,initialLearnRate、gradientDecayFactor squaredGradientDecayFactor);[fcParams, trailingAvgParams trailingAvgSqParams] =...adamupdate (fcParams gradientsParams,...trailingAvgParams trailingAvgSqParams,...迭代,initialLearnRate、gradientDecayFactor squaredGradientDecayFactor);如果~rem(iteration,validationFrequency) || iteration == 1 || iteration == numIterations监视器。状态=“验证”;精度= 0 (1,5);精度= 150;i = 1:5 [XAcc1,XAcc2,pairLabelsAcc] = getSiameseBatch(imdsTest,accuracyBatchSize);dlXAcc1 = dlarray(single(XAcc1),“SSCB”);dlXAcc2 = dlarray(single(XAcc2),“SSCB”);如果(输出。executionEnvironment = =“汽车”||...输出。executionEnvironment = =“图形”dlXAcc1 = gpuArray(dlXAcc1);dlXAcc2 = gpuArray(dlXAcc2);结束dlY = predictSiamese(dlnet,fcParams,dlXAcc1,dlXAcc2);Y = gather(extractdata(dlY));Y =圆(Y);precision (i) = sum(Y == pairLabelsAcc)/accuracyBatchSize;结束recordMetrics(监控、迭代...ValidationAccuracy =意味着(精度)* 100);班长。状态=“培训”结束Output.network = dlnet;输出。weights = fcParams;recordMetrics(监控、迭代...TrainingLoss = lossValue);班长。进度=(迭代/numIterations)*100;如果班长。停止返回结束结束结束

附录2:自定义培训助手函数

modelGradients函数以暹罗体作为输入dlnetwork对象,一对小批量输入数据dlX1而且dlX2,以及标明它们是否相似的标签。该函数返回关于网络中可学习参数的损失的梯度,以及预测和地面真相之间的二进制交叉熵损失。

函数[gradientsSubnet,gradientsParams,loss] = modelGradients(dlnet,fcParams,dlX1,dlX2,pairLabels) Y = forwardSiamese(dlnet,fcParams,dlX1,dlX2);loss = binarycrossentropy(Y,pairLabels);[gradientsSubnet,gradientsParams] = dlgradient(loss,dlnet.Learnables,fcParams);结束

该函数返回网络预测的二进制交叉熵损失值。

函数loss = binarycrossentropy(Y,pairLabels) precision =底层类型(Y);Y(Y < eps(精度))= eps(精度);Y(Y > 1 - eps(精度))= 1 - eps(精度);
loss = -pairLabels.*log(Y) - (1 -pairLabels)。*log(1 - Y);loss = sum(loss)/numel(pairLabels);结束

这个函数定义了子网络和fullyconnect而且乙状结肠这些操作结合起来形成了完整的暹罗网络。该函数接受网络结构和两个训练图像,并返回对相似(接近于1)或不相似(接近于0)的概率的预测。

函数Y = forward siamese (dlnet,fcParams,dlX1,dlX2) F1 = forward(dlnet,dlX1);F1 = sigmoid(F1);
F2 = forward(dlnet,dlX2);F2 = sigmoid(F2);
Y = abs(F1 - F2);Y = fulllyconnect (Y,fcParams.FcWeights,fcParams.FcBias);Y = sigmoid(Y);结束

这个函数返回随机选择的成对图像。平均而言,这个函数产生一个由相似和不相似对组成的平衡集。

函数[X1,X2,pairLabels] = getSiameseBatch(imds,miniBatchSize) pairLabels = 0 (1,miniBatchSize);imgSize = size(readimage(imds,1));X1 = 0 ([imgSize 1 miniBatchSize]);X2 = 0 ([imgSize 1 miniBatchSize]);i = 1:miniBatchSize select = rand(1);如果select < 0.5 [pairIdx1,pairIdx2,pairLabels(i)] = getSimilarPair(imds.Labels);其他的[pairIdx1,pairIdx2,pairLabels(i)] = getDissimilarPair(imds.Labels);结束X1(:,:,:,i) = imds.readimage(pairIdx1);X2(:,:,:,i) = imds.readimage(pairIdx2);结束结束

这个函数返回一个随机的索引对,这些图像属于同一类,并且相似的pair标签为1。

函数[pairIdx1,pairIdx2,label] = getSimilarPair(classLabel) classes = unique(classLabel);classChoice = randi(编号(类));idxs = find(classLabel==classes(classChoice));pairIdxChoice = randperm(数字(idxs),2);pairIdx1 = idxs(pairIdxChoice(1));pairIdx2 = idxs(pairIdxChoice(2));Label = 1;结束

这个函数返回属于不同类别的图像的随机索引对,并且不同的对标签为0。

函数[pairIdx1,pairIdx2,label] = getDissimilarPair(classLabel) classes = unique(classLabel);classesChoice = randperm(编号(类),2);idxs1 = find(classLabel==classes(classesChoice(1)));idxs2 = find(classLabel==classes(classesChoice(2)));pairIdx1Choice = randi(编号(idxs1));pairIdx2Choice = randi(编号(idxs2));pairIdx1 = idxs1(pairIdx1Choice);pairIdx2 = idxs2(pairIdx2Choice);Label = 0;结束

该函数使用经过训练的网络对两张图像的相似性进行预测。

函数Y = predictSiamese(dlnet,fcParams,dlX1,dlX2) F1 = predict(dlnet,dlX1);F1 = sigmoid(F1);
F2 = predict(dlnet,dlX2);F2 = sigmoid(F2);
Y = abs(F1 - F2);Y = fulllyconnect (Y,fcParams.FcWeights,fcParams.FcBias);Y = sigmoid(Y);结束

附录3:显示成对的测试图像

此函数创建一小批图像对,以帮助您直观地检查网络是否正确识别相似和不同的图像对。这个函数使用helper函数predictSiamese而且predictSiamese,均列于附录2

函数displayTestSet(trainingOutput) dlnet = trainingOutput.network;fcParams = trainingOutput.weights;executionEnvironment = trainingOutput.executionEnvironment;downloadFolder = tempdir;dataFolderTest = fullfile(下载文件夹,“images_evaluation”);imdsTest = imageDatastore(dataFolderTest,...IncludeSubfolders = true,...LabelSource =“没有”);files = imdsTest.Files;Parts = split(files,filesep);标签= join(parts(:,(end-2):(end-1)),“_”);imdsTest。标签=分类的(标签);testBatchSize = 10;[XTest1,XTest2,pairLabelsTest] = getSiameseBatch(imdsTest,testBatchSize);dlXTest1 = dlarray(single(XTest1),“SSCB”);dlXTest2 = dlarray(single(XTest2),“SSCB”);如果(executionEnvironment = =“汽车”&& canUseGPU) || executionEnvironment ==“图形”dlXTest1 = gpuArray(dlXTest1);dlXTest2 = gpuArray(dlXTest2);结束dlYScore = predictSiamese(dlnet,fcParams,dlXTest1,dlXTest2);YScore = gather(extractdata(dlYScore));YPred = round(YScore);XTest1 = extractdata(dlXTest1);XTest2 = extractdata(dlXTest2);plotRatio = 16/9;testingPlot =图;testingPlot.Position(3) = plotRatio*testingPlot.Position(4);testingPlot。可见=“上”i = 1:数字(pairLabelsTest)如果YPred(i) == 1 predLabel =“相似”其他的predLabel =“不同”结束如果pairLabelsTest(i) == YPred(i) testStr =“{暗绿色}\ bf \颜色正确\ rm \换行符”其他的testStr =“{红}\ bf \颜色错误\ rm \换行符”结束次要情节(2、5、我)imshow ([XTest1 (::,:, i) XTest2(::,:,我)]);标题(testStr +{黑}\颜色预测:“+ predLabel +“\ newlineScore:“+ YScore(我));结束

另请参阅

应用程序

功能

对象

相关的话题