整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HTML格式化标记

格式化

>格式化标记通常只能是产生几种不同文本的样式,但在语义上它们各自有着自己的特点

>如果你只是想有着一些自己的文本样式,可以尝试使用“常用html标记”里格式化的内容或css样式表

>但是这样会对搜索引擎不友好

>所以我们强调使用语义化标签,提供给浏览器的语义信息越多,浏览器就可以越好地把这些信息展示给用户。

## `<abbr></abbr>`

作用:用于指示该标签内的内容是一个缩写

注意点:

1. 常与全局属性title配合使用,这样可以在鼠标移动到该处时显示全称

`The <abbr title="People's Republic of China">PRC</abbr> was founded in 1949.`

2. 在浏览器内渲染会使其在内容底部加上短虚线

## `<address></address>`

作用:用于定义文档或文章作者/拥有者的联系信息

>如果 `<address>` 元素位于 `<body>` 元素内,则它表示文档联系信息。

>如果 `<address>` 元素位于 `<article>` 元素内,则是它表示文章的联系信息。

注意点:

1. address元素中的文本通常呈现为斜体,大多数浏览器会在address元素前后换行

2. address元素不应该用于描述通讯地址,除非它是练习信息的一部分

3. address元素元素通常连同其他信息被包含在footer元素中

```

<address>

Written by <a href="mailto:webmaster@example.com">Donald Duck</a>.<br>

Visit us at:<br>

Example.com<br>

Box 564, Disneyland<br>

USA

</address>

```

## `<b></b>`

作用:定义粗体文本

## `<bdi></bdi>`

作用:定义文本的文本方向,使其脱离周围文本的方向设置

注意点:

1. bdi指的是bidi隔离

2. 在发布用户评论或其它难以控制的内容时,可以使用

3. 需要与全局属性dir配合使用

## `<bdo></bdo>`

作用:定义文字方向

注意点:

1. 请与全局属性dir配合使用

```

<p>

如果您的浏览器支持 bi-directional override (bdo),下一行会从右向左输出 (rtl);

</p>

<bdo dir="rtl">

Here is some Hebrew text

</bdo>

```

## `<blockquote></blockquote>`

作用:`<blockquote>`标签用于定义块引用

注意点:

1. 标签内的所有文本都会从常规文本中分离出来,一般会上下换行,左右增加外边距,有时也会使用斜体

2. 换而言之,块引用拥有它们自己的空间

3. 可选属性:

`cite=url`规定引用来源

```

<blockquote cite="http://www.wwf.org">

WWF's ultimate goal is to build a future where people live in harmony with nature.

</blockquote>

```

主流浏览器均不支持cite属性,但是搜索引擎可以因此获得更多的信息

## `<q></q>`

作用:用于定义短引用

注意点:

1. 浏览器经常在引用内容的人左右添加引号

2. `<q>` 与 `<blockquote>` 的区别:

- `<q>` 标签在本质上与 `<blockquote>` 是一样的。不同之处在于它们的显示和应用。`<q>` 标签用于简短的行内引用。如果需要从周围内容分离出来比较长的部分(通常显示为缩进地块),请使用 `<blockquote>` 标签。

3. 在html4中,firefox和opera中q元素包含的文本必须以引号来开始和结束,但是IE却不支持这个规定,如果我们为了满足其它浏览器而添加了引号,那么在IE中就会显示两组引号。

4. 尽管如此,我们还是推荐使用q元素,因为它在文档处理和信息提取方面将会有很强的效果

5. 可选属性:

`cite=citation`定义引用的出处或来源(citation)

## `<cite></cite>`

作用:表示所含文本是对某个参考文献的引用

注意点:

1. 在显示上与blockquote元素类似,均是斜体

2. 但是它不会有上下左右的外边距

3. 通常情况下还要把引用包裹在一对`<a></a>`标签中,然后把超链接指向引用

`<cite><a href=URL>引用名</a></cite>`

>`<cite>` 标签还有一个隐藏的功能:它可以使你或者其他人从文档中自动摘录参考书目。我们可以很容易地想象一个浏览器,它能够自动整理引用表格,并把它们作为脚注或者独立的文档来显示。`<cite>` 标签的语义已经远远超过了改变它所包含的文本外观的作用;它使浏览器能够以各种实用的方式来向用户表达文档的内容。

## `<code></code>`

作用:定义计算机代码文本

注意点:

1. code元素并不能将元素内的内容以原样显示,浏览器仍然会解析内容而不跳过

2. code只是给内容的字体改为等宽字体,即它只是将内容转变为暗示这是计算机代码的内容

## `<var></var>`

作用:`<var>` 标签表示变量的名称,或者由用户提供的值。

注意点:

1. 用 `<var>` 标签标记的文本通常显示为斜体。

2. `<var>` 标签是计算机文档中应用的另一个小窍门,这个标签经常与 `<code>` 和 `<pre>` 标签一起使用,用来显示计算机编程代码范例及类似方面的特定元素。

## `<smap></smap>`

作用:用于从一段上下文中抽取一些字符

例子:

`字符序列 <samp>ae</samp> 可能会被转换为 æ 连字字符。`

效果:

`字符序列 ae 可能会被转换为 æ 连字字符。`

## `<ins></ins>`

作用:定义一个插入文本

注意点:

1. 显示效果是加入下划线

## `<dfn></dfn>`

作用:用于标记特殊术语或短语

注意点:

1. 浏览器通常会将dfn元素内的内容显示为斜体

2. 应当尽量少的使用,比如在技术性的文档中,在第一次提到一个术语时,可以加上dfn元素,而在相同文档的后续中,对于同一个术语,应避免使用dfn

## `<em></em>`

作用:定义一个强调文本

注意点:

1. 在显示结果上,它依然是斜体

2. 如果你只是为了定义一个斜体的内容,可以考虑使用`<i></i>`或css样式表

3. 对于强调的内容应当不宜过多,否则无法突出想要表达的内容

## `<strong></strong>`

作用:定义一个语气更加强烈地强调文本

注意点:

1. 常识告诉我们应较少使用em元素的话,那么strong元素出现的次数应该更少,限制其使用可以让这个标记更加的引人注意和有效

## `<i></i>`

作用:定义一个斜体文本或倾斜的文本

## `<kbd></kbd>`

作用:定义键盘文本

注意点:

1. 显示效果为等宽字体

## `<mark></mark>`(HTML5)

作用:定义带有记号的文本

注意点:

1. 与加粗不同,它会将文字加上背景色

## `<meter></meter>`(HTML5)

作用:定义已知范围或分数值内的标量测量,也被称为gauge(尺度)

注意点:

1. 这是一个html5的新标签,假如你把文档类型声明为html4或以下和xhtml,标签本身的效果将会受到影响

2. `<meter>`标签不应用于指示进度条的进度,如果标记进度,请使用`<progress></progress>`标签

可用属性:

1. form=form_id-->规定meter元素所属的一个或多个表单

2. high=number-->规定被视作高的值的范围

3. low=number-->规定被视作高的值的范围

4. max=number-->规定范围的最大值

5. min=number-->规定范围的最小值

6. optimum=number-->规定度量的优化值

7. value=number-->必须。规定度量的当前值

```

<meter value="3" min="0" max="10">十分之三</meter>

<meter value="0.6">60%</meter>

```

## `<progress></progress>`(HTML5)

作用:标示任务的进度(进程)

`<progress value="22" max="100"></progress> `

注意点:

1. 需要与js结合使用,来显示任务的进度

2. progress标签不适合使用来表示度量衡,这种情况请使用meter元素来替代

可用属性:

max=number-->规定任务一共需要多少工作

value=number-->规定任务已经完成多少工作

## `<pre></pre>`

作用:用于定义预格式化的文本,即通常会保留空格和换行,文本会呈现为等宽字体

注意点:

1. pre元素常用于表示计算机的源代码,但是计算机的源代码(html)直接放入浏览器仍会解析(需要使用`<`和`>`等符号实体)

2. 会导致内容截断的标签绝不能包含在pre元素中,如标题、p元素、address元素

3. 可选的属性:

`width=number`定义每行的最大字符数

## `<ruby></ruby>`

作用:可用于定义一个ruby注释(中文注音或字符)

注意点:

1. ruby元素与rt元素一同使用

2. ruby元素由需要一组字符和一个提供信息的rt元素组成

3. 还包括一个可选的rp元素,定义浏览器不支持ruby元素时显示的内容

## `<rt></rt>`

作用:定义字符的解释或发音

## `<rp></rp>`

作用:定义浏览器不支持ruby元素时显示的内容

## `<s></s>`

作用:定义加删除线的文本

注意点:

1. `<s>`标签是`<strike>`标签的缩写版本,但html4和xhtml中已经不再赞成使用它了,就是说,它早晚有一天会消失

2. 请使用`<del></del>`替代它

## `<del></del>`

作用:给元素中的内容上加上删除线

注意点:

1. 请与`<ins></ins>`标签配合使用,来描述文档中的更新与修正

2. 可选的属性:

- cite=URL

- datetime=YYYMMDD (定义文本被删除的日期和时间)

## `<small></small>`

作用:标签内的元素呈现小号字体的效果

注意点:

1. 如果被包裹字体已经是最小号的字体了,那这个标签将不起任何作用

2. `<small></small>`是可以嵌套的,从而把文字连续的缩小,直到到达最小的一号字

## `<sup></sup>`

作用:标签中的内容会以当前文本流中字符高度的一般来显示(上标)

注意点:

1. 虽然显示效果与文本流中其他元素不一样,但是它们的字体字号都是一样的

2. 这个标签在向文档添加注脚以及表示方程式中的指数时非常有效,如果与`<a></a>`标签结合使用可以创建出很好的超链接注脚

## `<sub></sub>`

作用:标签中的内容会以当前文本流中字符高度的一般来显示(下标)

## `<template></template>`

作用:可以作为一个容器,但是它并不会存在于DOM树中

注意点:

1. 多用于包裹一段代码,对其绑定事件,使其可以控制是否隐藏

2. 一个检查方法:

```

if (document.createElement("template").content) {

document.write("Your browser supports template!");

} else {

document.write("您的浏览器不支持 template!");

}

```

## `<u></u>`

作用:定义下划线文本

注意点:

1. 应尽量避免使用,用户可能会把它混淆为一个超链接

## `<time></time>`(HTML5)

作用:定义一个公历的时间或日期,时间和时区偏移是可选的

可选的属性:

1. datetime=datetime-->规定日期/时间。否则由元素内容给定日期时间

2. pubdate=pubdate-->指示 `<time>` 元素中的日期 / 时间是文档(或 `<article>` 元素)的发布日期。

## `<wbr>`(HTML5)

一段带有 Word Break Opportunity 的文本:

```

<p>

如果想学习 AJAX,那么您必须熟悉 XML<wbr>Http<wbr>Request 对象。

</p>

```

作用:Word Break Opportunity (`<wbr>`) 规定在文本中的何处适合添加换行符。

注意点:如果单词太长,或者您担心浏览器会在错误的位置换行,那么您可以使用 `<wbr>` 元素来添加 Word Break Opportunity(单词换行时机)。

言:授人以鱼不如授人以渔.先学会用,在学原理,在学创造,可能一辈子用不到这种能力,但是不能不具备这种能力。这篇文章主要是介绍算法入门Helloword之手写图片识别模型java中如何实现以及部分解释。目前大家对于人工智能-机器学习-神经网络的文章都是基于python语言的,对于擅长java的后端小伙伴想要去了解就不是特别友好,所以这里给大家介绍一下如何在java中实现,打开新世界的大门。以下为本人个人理解如有错误欢迎指正

一、目标:使用MNIST数据集训练手写数字图片识别模型

在实现一个模型的时候我们要准备哪些知识体系:

1.机器学习基础:包括监督学习、无监督学习、强化学习等基本概念。

2.数据处理与分析:数据清洗、特征工程、数据可视化等。

3.编程语言:如Python,用于实现机器学习算法。

4.数学基础:线性代数、概率统计、微积分等数学知识。

5.机器学习算法:线性回归、决策树、神经网络、支持向量机等算法。

6.深度学习框架:如TensorFlow、PyTorch等,用于构建和训练深度学习模型。

7.模型评估与优化:交叉验证、超参数调优、模型评估指标等。

8.实践经验:通过实际项目和竞赛积累经验,不断提升模型学习能力。

这里的机器学习HelloWorld是手写图片识别用的是TensorFlow框架

主要需要:

1.理解手写图片的数据集,训练集是什么样的数据(60000,28,28) 、训练集的标签是什么样的(1)

2.理解激活函数的作用

3.正向传递和反向传播的作用以及实现

4.训练模型和保存模型

5.加载保存的模型使用

二、java代码与python代码对比分析

因为python代码解释网上已经有很多了,这里不在重复解释

1.数据集的加载

python中

def load_data(dpata_folder):
    files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
             "t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
    paths = []
    for fname in files:
        paths.append(os.path.join(data_folder, fname))
    with gzip.open(paths[0], 'rb') as lbpath:
        train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
    with gzip.open(paths[1], 'rb') as imgpath:
        train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)
    with gzip.open(paths[2], 'rb') as lbpath:
        test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
    with gzip.open(paths[3], 'rb') as imgpath:
        test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)
    return (train_x, train_y), (test_x, test_y)
(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print('\n train_x:%s, train_y:%s, test_x:%s, test_y:%s' % (train_x.shape, train_y.shape, test_x.shape, test_y.shape))
print(train_x.ndim)  # 数据集的维度
print(train_x.shape)  # 数据集的形状
print(len(train_x))  # 数据集的大小
print(train_x)  # 数据集
print("---查看单个数据")
print(train_x[0])
print(len(train_x[0]))
print(len(train_x[0][1]))
print(train_x[0][6])
print("---查看单个数据")
print(train_y[3])



java中

SimpleMnist.class

 private static final String TRAINING_IMAGES_ARCHIVE = "mnist/train-images-idx3-ubyte.gz";
    private static final String TRAINING_LABELS_ARCHIVE = "mnist/train-labels-idx1-ubyte.gz";
    private static final String TEST_IMAGES_ARCHIVE = "mnist/t10k-images-idx3-ubyte.gz";
    private static final String TEST_LABELS_ARCHIVE = "mnist/t10k-labels-idx1-ubyte.gz";
//加载数据
MnistDataset validationDataset = MnistDataset.getOneValidationImage(3, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);

MnistDataset.class

  /**
     * @param trainingImagesArchive 训练图片路径
     * @param trainingLabelsArchive 训练标签路径
     * @param testImagesArchive     测试图片路径
     * @param testLabelsArchive     测试标签路径
     */
    public static MnistDataset getOneValidationImage(int index, String trainingImagesArchive, String trainingLabelsArchive,String testImagesArchive, String testLabelsArchive) {
        try {
            ByteNdArray trainingImages = readArchive(trainingImagesArchive);
            ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
            ByteNdArray testImages = readArchive(testImagesArchive);
            ByteNdArray testLabels = readArchive(testLabelsArchive);
            trainingImages.slice(sliceFrom(0));
            trainingLabels.slice(sliceTo(0));
            // 切片操作
            Index range = Indices.range(index, index + 1);// 切片的起始和结束索引
            ByteNdArray validationImage = trainingImages.slice(range); // 执行切片操作
            ByteNdArray validationLable = trainingLabels.slice(range); // 执行切片操作
            if (index >= 0) {
                return new MnistDataset(trainingImages,trainingLabels,validationImage,validationLable,testImages,testLabels);
            } else {
                return null;
            }
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }  
    private static ByteNdArray readArchive(String archiveName) throws IOException {
        System.out.println("archiveName = " + archiveName);
        DataInputStream archiveStream = new DataInputStream(new GZIPInputStream(MnistDataset.class.getClassLoader().getResourceAsStream(archiveName))
        );
        archiveStream.readShort(); // first two bytes are always 0
        byte magic = archiveStream.readByte();
        if (magic != TYPE_UBYTE) {
            throw new IllegalArgumentException("\"" + archiveName + "\" is not a valid archive");
        }
        int numDims = archiveStream.readByte();
        long[] dimSizes = new long[numDims];
        int size = 1;  // for simplicity, we assume that total size does not exceeds Integer.MAX_VALUE
        for (int i = 0; i < dimSizes.length; ++i) {
            dimSizes[i] = archiveStream.readInt();
            size *= dimSizes[i];
        }
        byte[] bytes = new byte[size];
        archiveStream.readFully(bytes);
        return NdArrays.wrap(Shape.of(dimSizes), DataBuffers.of(bytes, false, false));
    }
    /**
     * Mnist 数据集构造器
     */
    private MnistDataset(ByteNdArray trainingImages, ByteNdArray trainingLabels,ByteNdArray validationImages,ByteNdArray validationLabels,ByteNdArray testImages,ByteNdArray testLabels
    ) {
        this.trainingImages = trainingImages;
        this.trainingLabels = trainingLabels;
        this.validationImages = validationImages;
        this.validationLabels = validationLabels;
        this.testImages = testImages;
        this.testLabels = testLabels;
        this.imageSize = trainingImages.get(0).shape().size();
        System.out.println(String.format("train_x:%s,train_y:%s, test_x:%s, test_y:%s", trainingImages.shape(), trainingLabels.shape(), testImages.shape(), testLabels.shape()));
        System.out.println("数据集的维度:" + trainingImages.rank());
        System.out.println("数据集的形状 = " + trainingImages.shape());
        System.out.println("数据集的大小 = " + trainingImages.shape().get(0));
        System.out.println("查看单个数据 = " + trainingImages.get(0));
    }



2.模型构建

python中

model = tensorflow.keras.Sequential()
model.add(tensorflow.keras.layers.Flatten(input_shape=(28, 28)))  # 添加Flatten层说明输入数据的形状
model.add(tensorflow.keras.layers.Dense(128, activation='relu'))  # 添加隐含层,为全连接层,128个节点,relu激活函数
model.add(tensorflow.keras.layers.Dense(10, activation='softmax'))  # 添加输出层,为全连接层,10个节点,softmax激活函数
print("打印模型结构")
# 使用 summary 打印模型结构
print('\n', model.summary())  # 查看网络结构和参数信息
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])

java中

SimpleMnist.class

        Ops tf = Ops.create(graph);
        // Create placeholders and variables, which should fit batches of an unknown number of images
        //创建占位符和变量,这些占位符和变量应适合未知数量的图像批次
        Placeholder<TFloat32> images = tf.placeholder(TFloat32.class);
        Placeholder<TFloat32> labels = tf.placeholder(TFloat32.class);

        // Create weights with an initial value of 0
        // 创建初始值为 0 的权重
        Shape weightShape = Shape.of(dataset.imageSize(), MnistDataset.NUM_CLASSES);
        Variable<TFloat32> weights = tf.variable(tf.zeros(tf.constant(weightShape), TFloat32.class));
        
        // Create biases with an initial value of 0
        //创建初始值为 0 的偏置
        Shape biasShape = Shape.of(MnistDataset.NUM_CLASSES);
        Variable<TFloat32> biases = tf.variable(tf.zeros(tf.constant(biasShape), TFloat32.class));

        // Predict the class of each image in the batch and compute the loss
        //使用 TensorFlow 的 tf.linalg.matMul 函数计算图像矩阵 images 和权重矩阵 weights 的矩阵乘法,并加上偏置项 biases。
        //wx+b
        MatMul<TFloat32> matMul = tf.linalg.matMul(images, weights);
        Add<TFloat32> add = tf.math.add(matMul, biases);
        //Softmax 是一个常用的激活函数,它将输入转换为表示概率分布的输出。对于输入向量中的每个元素,Softmax 函数会计算指数,
        //并对所有元素求和,然后将每个元素的指数除以总和,最终得到一个概率分布。这通常用于多分类问题,以输出每个类别的概率
        Softmax<TFloat32> softmax = tf.nn.softmax(add);

        // 创建一个计算交叉熵的Mean对象
        Mean<TFloat32> crossEntropy =
                tf.math.mean(  // 计算张量的平均值
                        tf.math.neg(  // 计算张量的负值
                                tf.reduceSum(  // 计算张量的和
                                        tf.math.mul(labels, tf.math.log(softmax)),  //计算标签和softmax预测的对数乘积
                                        tf.array(1)  // 在指定轴上求和
                                )
                        ),
                        tf.array(0)  // 在指定轴上求平均值
                );

        // Back-propagate gradients to variables for training
        //使用梯度下降优化器来最小化交叉熵损失函数。首先,创建了一个梯度下降优化器 optimizer,然后使用该优化器来最小化交叉熵损失函数 crossEntropy。
        Optimizer optimizer = new GradientDescent(graph, LEARNING_RATE);
        Op minimize = optimizer.minimize(crossEntropy);

3.训练模型

python中

history = model.fit(train_x, train_y, batch_size=64, epochs=5, validation_split=0.2)

java中

SimpleMnist.class

            // Train the model
            for (ImageBatch trainingBatch : dataset.trainingBatches(TRAINING_BATCH_SIZE)) {
                try (TFloat32 batchImages = preprocessImages(trainingBatch.images());
                     TFloat32 batchLabels = preprocessLabels(trainingBatch.labels())) {
                    // 创建会话运行器
                    session.runner()
                            // 添加要最小化的目标
                            .addTarget(minimize)
                            // 通过feed方法将图像数据输入到模型中
                            .feed(images.asOutput(), batchImages)
                            // 通过feed方法将标签数据输入到模型中
                            .feed(labels.asOutput(), batchLabels)
                            // 运行会话
                            .run();
                }
            }

4.模型评估

python中

test_loss, test_acc = model.evaluate(test_x, test_y)
model.evaluate(test_x, test_y, verbose=2)  # 每次迭代输出一条记录,来评价该模型是否有比较好的泛化能力
print('Test 损失: %.3f' % test_loss)
print('Test 精确度: %.3f' % test_acc)

java中

SimpleMnist.class

   // Test the model
            ImageBatch testBatch = dataset.testBatch();
            try (TFloat32 testImages = preprocessImages(testBatch.images());
                 TFloat32 testLabels = preprocessLabels(testBatch.labels());
                 // 定义一个TFloat32类型的变量accuracyValue,用于存储计算得到的准确率值
                 TFloat32 accuracyValue = (TFloat32) session.runner()
                         // 从会话中获取准确率值
                         .fetch(accuracy)
                         .fetch(predicted)
                         .fetch(expected)
                         // 将images作为输入,testImages作为数据进行喂养
                         .feed(images.asOutput(), testImages)
                         // 将labels作为输入,testLabels作为数据进行喂养
                         .feed(labels.asOutput(), testLabels)
                         // 运行会话并获取结果
                         .run()
                         // 获取第一个结果并存储在accuracyValue中
                         .get(0)) {
                System.out.println("Accuracy: " + accuracyValue.getFloat());
            }

5.保存模型

python中

# 使用save_model保存完整模型
# save_model(model, '/media/cfs/用户ERP名称/ea/saved_model', save_format='pb')
save_model(model, 'D:\\pythonProject\\mnistDemo\\number_model', save_format='pb')

java中

SimpleMnist.class

            // 保存模型
            SavedModelBundle.Exporter exporter = SavedModelBundle.exporter("D:\\ai\\ai-demo").withSession(session);
            Signature.Builder builder = Signature.builder();
            builder.input("images", images);
            builder.input("labels", labels);
            builder.output("accuracy", accuracy);
            builder.output("expected", expected);
            builder.output("predicted", predicted);
            Signature signature = builder.build();
            SessionFunction sessionFunction = SessionFunction.create(signature, session);
            exporter.withFunction(sessionFunction);
            exporter.export();

6.加载模型

python中

 # 加载.pb模型文件
    global load_model
    load_model = load_model('D:\\pythonProject\\mnistDemo\\number_model')
    load_model.summary()
    demo = tensorflow.reshape(test_x, (1, 28, 28))
    input_data = np.array(demo)  # 准备你的输入数据
    input_data = tensorflow.convert_to_tensor(input_data, dtype=tensorflow.float32)
    predictValue = load_model.predict(input_data)
    print("predictValue")
    print(predictValue)
    y_pred = np.argmax(predictValue)
    print('标签值:' + str(test_y) + '\n预测值:' + str(y_pred))
    return y_pred, test_y,

java中

SimpleMnist.class

	//加载模型并预测
    public void loadModel(String exportDir) {
        // load saved model
        SavedModelBundle model = SavedModelBundle.load(exportDir, "serve");
        try {
            printSignature(model);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        ByteNdArray validationImages = dataset.getValidationImages();
        ByteNdArray validationLabels = dataset.getValidationLabels();
        TFloat32 testImages = preprocessImages(validationImages);
        System.out.println("testImages = " + testImages.shape());
        TFloat32 testLabels = preprocessLabels(validationLabels);
        System.out.println("testLabels = " + testLabels.shape());
        Result run = model.session().runner()
                .feed("Placeholder:0", testImages)
                .feed("Placeholder_1:0", testLabels)
                .fetch("ArgMax:0")
                .fetch("ArgMax_1:0")
                .fetch("Mean_1:0")
                .run();
        // 处理输出
        Optional<Tensor> tensor1 = run.get("ArgMax:0");
        Optional<Tensor> tensor2 = run.get("ArgMax_1:0");
        Optional<Tensor> tensor3 = run.get("Mean_1:0");
        TInt64 predicted = (TInt64) tensor1.get();
        Long predictedValue = predicted.getObject(0);
        System.out.println("predictedValue = " + predictedValue);
        TInt64 expected = (TInt64) tensor2.get();
        Long expectedValue = expected.getObject(0);
        System.out.println("expectedValue = " + expectedValue);
        TFloat32 accuracy = (TFloat32) tensor3.get();
        System.out.println("accuracy = " + accuracy.getFloat());
    }
    //打印模型信息
    private static void printSignature(SavedModelBundle model) throws Exception {
        MetaGraphDef m = model.metaGraphDef();
        SignatureDef sig = m.getSignatureDefOrThrow("serving_default");
        int numInputs = sig.getInputsCount();
        int i = 1;
        System.out.println("MODEL SIGNATURE");
        System.out.println("Inputs:");
        for (Map.Entry<String, TensorInfo> entry : sig.getInputsMap().entrySet()) {
            TensorInfo t = entry.getValue();
            System.out.printf(
                    "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
                    i++, numInputs, entry.getKey(), t.getName(), t.getDtype());
        }
        int numOutputs = sig.getOutputsCount();
        i = 1;
        System.out.println("Outputs:");
        for (Map.Entry<String, TensorInfo> entry : sig.getOutputsMap().entrySet()) {
            TensorInfo t = entry.getValue();
            System.out.printf(
                    "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
                    i++, numOutputs, entry.getKey(), t.getName(), t.getDtype());
        }
    }

三、完整的python代码

本工程使用环境为

Python: 3.7.9

https://www.python.org/downloads/windows/

Anaconda: Python 3.11 Anaconda3-2023.09-0-Windows-x86_64

https://www.anaconda.com/download#downloads

tensorflow:2.0.0

直接从anaconda下安装

mnistTrainDemo.py

import gzip
import os.path
import tensorflow as tensorflow
from tensorflow import keras
# 可视化 image
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.models import save_model

# 加载数据
# mnist = keras.datasets.mnist
# mnistData = mnist.load_data() #Exception: URL fetch failure on https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz: None -- unknown url type: https
"""
这里可以直接使用
mnist = keras.datasets.mnist
mnistData = mnist.load_data() 加载数据,但是有的时候不成功,所以使用本地加载数据
"""
def load_data(data_folder):
    files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
             "t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
    paths = []
    for fname in files:
        paths.append(os.path.join(data_folder, fname))

    with gzip.open(paths[0], 'rb') as lbpath:
        train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)

    with gzip.open(paths[1], 'rb') as imgpath:
        train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)

    with gzip.open(paths[2], 'rb') as lbpath:
        test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)

    with gzip.open(paths[3], 'rb') as imgpath:
        test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)

    return (train_x, train_y), (test_x, test_y)

(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print('\n train_x:%s, train_y:%s, test_x:%s, test_y:%s' % (train_x.shape, train_y.shape, test_x.shape, test_y.shape))
print(train_x.ndim)  # 数据集的维度
print(train_x.shape)  # 数据集的形状
print(len(train_x))  # 数据集的大小
print(train_x)  # 数据集
print("---查看单个数据")
print(train_x[0])
print(len(train_x[0]))
print(len(train_x[0][1]))
print(train_x[0][6])
# 可视化image图片、一副image的数据
# plt.imshow(train_x[0].reshape(28, 28), cmap="binary")
# plt.show()
print("---查看单个数据")
print(train_y[0])

# 数据预处理
# 归一化、并转换为tensor张量,数据类型为float32.  ---归一化也可能造成识别率低
# train_x, test_x = tensorflow.cast(train_x / 255.0, tensorflow.float32), tensorflow.cast(test_x / 255.0,
#                                                                                         tensorflow.float32),
# train_y, test_y = tensorflow.cast(train_y, tensorflow.int16), tensorflow.cast(test_y, tensorflow.int16)
# print("---查看单个数据归一后的数据")
# print(train_x[0][6])  # 30/255=0.11764706  ---归一化每个值除以255
# print(train_y[0])

# Step2: 配置网络 建立模型
'''
以下的代码判断就是定义一个简单的多层感知器,一共有三层,
两个大小为100的隐层和一个大小为10的输出层,因为MNIST数据集是手写0到9的灰度图像,
类别有10个,所以最后的输出大小是10。最后输出层的激活函数是Softmax,
所以最后的输出层相当于一个分类器。加上一个输入层的话,
多层感知器的结构是:输入层-->>隐层-->>隐层-->>输出层。
激活函数 https://zhuanlan.zhihu.com/p/337902763
'''
# 构造模型
# model = keras.Sequential([
#     # 在第一层的网络中,我们的输入形状是28*28,这里的形状就是图片的长度和宽度。
#     keras.layers.Flatten(input_shape=(28, 28)),
#     # 所以神经网络有点像滤波器(过滤装置),输入一组28*28像素的图片后,输出10个类别的判断结果。那这个128的数字是做什么用的呢?
#     # 我们可以这样想象,神经网络中有128个函数,每个函数都有自己的参数。
#     # 我们给这些函数进行一个编号,f0,f1…f127 ,我们想的是当图片的像素一一带入这128个函数后,这些函数的组合最终输出一个标签值,在这个样例中,我们希望它输出09 。
#     # 为了得到这个结果,计算机必须要搞清楚这128个函数的具体参数,之后才能计算各个图片的标签。这里的逻辑是,一旦计算机搞清楚了这些参数,那它就能够认出不同的10个类别的事物了。
#     keras.layers.Dense(100, activation=tensorflow.nn.relu),
#     # 最后一层是10,是数据集中各种类别的代号,数据集总共有10类,这里就是10 。
#     keras.layers.Dense(10, activation=tensorflow.nn.softmax)
# ])

model = tensorflow.keras.Sequential()
model.add(tensorflow.keras.layers.Flatten(input_shape=(28, 28)))  # 添加Flatten层说明输入数据的形状
model.add(tensorflow.keras.layers.Dense(128, activation='relu'))  # 添加隐含层,为全连接层,128个节点,relu激活函数
model.add(tensorflow.keras.layers.Dense(10, activation='softmax'))  # 添加输出层,为全连接层,10个节点,softmax激活函数
print("打印模型结构")
# 使用 summary 打印模型结构
# print(model.summary())
print('\n', model.summary())  # 查看网络结构和参数信息

'''
接着是配置模型,在这一步,我们需要指定模型训练时所使用的优化算法与损失函数,
此外,这里我们也可以定义计算精度相关的API。
优化器https://zhuanlan.zhihu.com/p/27449596
'''
# 配置模型  配置模型训练方法
# 设置神经网络的优化器和损失函数。# 使用Adam算法进行优化   # 使用CrossEntropyLoss 计算损失 # 使用Accuracy 计算精度
# model.compile(optimizer=tensorflow.optimizers.Adam(), loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# adam算法参数采用keras默认的公开参数,损失函数采用稀疏交叉熵损失函数,准确率采用稀疏分类准确率函数
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])

# Step3:模型训练
# 开始模型训练
# model.fit(x_train,  # 设置训练数据集
#           y_train,
#           epochs=5,  # 设置训练轮数
#           batch_size=64,  # 设置 batch_size
#           verbose=1)  # 设置日志打印格式
# 批量训练大小为64,迭代5次,测试集比例0.2(48000条训练集数据,12000条测试集数据)
history = model.fit(train_x, train_y, batch_size=64, epochs=5, validation_split=0.2)

# STEP4: 模型评估
# 评估模型,不输出预测结果输出损失和精确度. test_loss损失,test_acc精确度
test_loss, test_acc = model.evaluate(test_x, test_y)
model.evaluate(test_x, test_y, verbose=2)  # 每次迭代输出一条记录,来评价该模型是否有比较好的泛化能力
# model.evaluate(test_dataset, verbose=1)
print('Test 损失: %.3f' % test_loss)
print('Test 精确度: %.3f' % test_acc)
# 结果可视化
print(history.history)
loss = history.history['loss']  # 训练集损失
val_loss = history.history['val_loss']  # 测试集损失
acc = history.history['sparse_categorical_accuracy']  # 训练集准确率
val_acc = history.history['val_sparse_categorical_accuracy']  # 测试集准确率

plt.figure(figsize=(10, 3))
plt.subplot(121)
plt.plot(loss, color='b', label='train')
plt.plot(val_loss, color='r', label='test')
plt.ylabel('loss')
plt.legend()

plt.subplot(122)
plt.plot(acc, color='b', label='train')
plt.plot(val_acc, color='r', label='test')
plt.ylabel('Accuracy')
plt.legend()

# 暂停5秒关闭画布,否则画布一直打开的同时,会持续占用GPU内存
# plt.ion()  # 打开交互式操作模式
# plt.show()
# plt.pause(5)
# plt.close()
# plt.show()

# Step5:模型预测 输入测试数据,输出预测结果
for i in range(1):
    num = np.random.randint(1, 10000)  # 在1~10000之间生成随机整数
    plt.subplot(2, 5, i + 1)
    plt.axis('off')
    plt.imshow(test_x[num], cmap='gray')
    demo = tensorflow.reshape(test_x[num], (1, 28, 28))
    y_pred = np.argmax(model.predict(demo))
    plt.title('标签值:' + str(test_y[num]) + '\n预测值:' + str(y_pred))
# plt.show()

'''
保存模型
训练好的模型可以用于加载后对新输入数据进行预测,所以需要先进行保存已训练模型
'''
#使用save_model保存完整模型
save_model(model, 'D:\\pythonProject\\mnistDemo\\number_model', save_format='pb')

mnistPredictDemo.py

import numpy as np
import tensorflow as tensorflow
import gzip
import os.path
from tensorflow.keras.models import load_model
# 预测
def predict(test_x, test_y):
    test_x, test_y = test_x, test_y
    '''
    五、模型评估
    需要先加载已训练模型,然后用其预测新的数据,计算评估指标
    '''
    # 模型加载
    # 加载.pb模型文件
    global load_model
    # load_model = load_model('./saved_model')
    load_model = load_model('D:\\pythonProject\\mnistDemo\\number_model')
    load_model.summary()
    # make a prediction
    print("test_x")
    print(test_x)
    print(test_x.ndim)
    print(test_x.shape)

    demo = tensorflow.reshape(test_x, (1, 28, 28))
    input_data = np.array(demo)  # 准备你的输入数据
    input_data = tensorflow.convert_to_tensor(input_data, dtype=tensorflow.float32)
    # test_x = tensorflow.cast(test_x / 255.0, tensorflow.float32)
    # test_y = tensorflow.cast(test_y, tensorflow.int16)
    predictValue = load_model.predict(input_data)
    print("predictValue")
    print(predictValue)
    y_pred = np.argmax(predictValue)
    print('标签值:' + str(test_y) + '\n预测值:' + str(y_pred))
    return y_pred, test_y,
  
def load_data(data_folder):
    files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
             "t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
    paths = []
    for fname in files:
        paths.append(os.path.join(data_folder, fname))
    with gzip.open(paths[0], 'rb') as lbpath:
        train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
    with gzip.open(paths[1], 'rb') as imgpath:
        train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)
    with gzip.open(paths[2], 'rb') as lbpath:
        test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
    with gzip.open(paths[3], 'rb') as imgpath:
        test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)
    return (train_x, train_y), (test_x, test_y)

(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print(train_x[0])
predict(train_x[0], train_y)

四、完整的java代码

tensorflow 需要的java 版本对应表: https://github.com/tensorflow/java/#tensorflow-version-support

本工程使用环境为

jdk版本:openjdk-21

pom依赖如下:


        <dependency>
            <groupId>org.tensorflow</groupId>
            <artifactId>tensorflow-core-platform</artifactId>
            <version>0.6.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.tensorflow</groupId>
            <artifactId>tensorflow-framework</artifactId>
            <version>0.6.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>tensorflow-snapshots</id>
            <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

数据集创建和解析类

MnistDataset.class

package org.example.tensorDemo.datasets.mnist;

import org.example.tensorDemo.datasets.ImageBatch;
import org.example.tensorDemo.datasets.ImageBatchIterator;
import org.tensorflow.ndarray.*;
import org.tensorflow.ndarray.buffer.DataBuffers;
import org.tensorflow.ndarray.index.Index;
import org.tensorflow.ndarray.index.Indices;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;

import static org.tensorflow.ndarray.index.Indices.sliceFrom;
import static org.tensorflow.ndarray.index.Indices.sliceTo;



public class MnistDataset {
    public static final int NUM_CLASSES = 10;

    private static final int TYPE_UBYTE = 0x08;

    /**
     * 训练图片字节类型的多维数组
     */
    private final ByteNdArray trainingImages;

    /**
     * 训练标签字节类型的多维数组
     */
    private final ByteNdArray trainingLabels;

    /**
     * 验证图片字节类型的多维数组
     */
    public final ByteNdArray validationImages;

    /**
     * 验证标签字节类型的多维数组
     */
    public final ByteNdArray validationLabels;

    /**
     * 测试图片字节类型的多维数组
     */
    private final ByteNdArray testImages;

    /**
     * 测试标签字节类型的多维数组
     */
    private final ByteNdArray testLabels;

    /**
     * 图片的大小
     */
    private final long imageSize;


    /**
     * Mnist 数据集构造器
     */
    private MnistDataset(
            ByteNdArray trainingImages,
            ByteNdArray trainingLabels,
            ByteNdArray validationImages,
            ByteNdArray validationLabels,
            ByteNdArray testImages,
            ByteNdArray testLabels
    ) {
        this.trainingImages = trainingImages;
        this.trainingLabels = trainingLabels;
        this.validationImages = validationImages;
        this.validationLabels = validationLabels;
        this.testImages = testImages;
        this.testLabels = testLabels;
        //第一个图像的形状,并返回其尺寸大小。每一张图片包含28X28个像素点 所以应该为784
        this.imageSize = trainingImages.get(0).shape().size();
//        System.out.println("imageSize = " + imageSize);


//        System.out.println(String.format("train_x:%s,train_y:%s, test_x:%s, test_y:%s", trainingImages.shape(), trainingLabels.shape(), testImages.shape(), testLabels.shape()));
//        System.out.println("数据集的维度:" + trainingImages.rank());
//        System.out.println("数据集的形状 = " + trainingImages.shape());
//        System.out.println("数据集的大小 = " + trainingImages.shape().get(0));
//        System.out.println("数据集 = ");
//        for (int i = 0; i < trainingImages.shape().get(0); i++) {
//            for (int j = 0; j < trainingImages.shape().get(1); j++) {
//                for (int k = 0; k < trainingImages.shape().get(2); k++) {
//                    System.out.print(trainingImages.getObject(i, j, k) + " ");
//                }
//                System.out.println();
//            }
//            System.out.println();
//        }
//        System.out.println("查看单个数据 = " + trainingImages.get(0));
//        for (int j = 0; j < trainingImages.shape().get(1); j++) {
//            for (int k = 0; k < trainingImages.shape().get(2); k++) {
//                System.out.print(trainingImages.getObject(0, j, k) + " ");
//            }
//            System.out.println();
//        }
//        System.out.println("查看单个数据大小 = " + trainingImages.get(0).size());
//        System.out.println("查看trainingImages三维数组下的第一个元素的第二个二维数组大小 = " + trainingImages.get(0).get(1).size());
//        System.out.println("查看trainingImages三维数组下的第一个元素的第7个二维数组的第8个元素 = " + trainingImages.getObject(0, 6, 8));
//        System.out.println("trainingLabels = " + trainingLabels.getObject(1));
    }

    /**
     * @param validationSize        验证的数据
     * @param trainingImagesArchive 训练图片路径
     * @param trainingLabelsArchive 训练标签路径
     * @param testImagesArchive     测试图片路径
     * @param testLabelsArchive     测试标签路径
     */
    public static MnistDataset create(int validationSize, String trainingImagesArchive, String trainingLabelsArchive,
                                      String testImagesArchive, String testLabelsArchive) {
        try {
            ByteNdArray trainingImages = readArchive(trainingImagesArchive);
            ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
            ByteNdArray testImages = readArchive(testImagesArchive);
            ByteNdArray testLabels = readArchive(testLabelsArchive);

            if (validationSize > 0) {
                return new MnistDataset(
                        trainingImages.slice(sliceFrom(validationSize)),
                        trainingLabels.slice(sliceFrom(validationSize)),
                        trainingImages.slice(sliceTo(validationSize)),
                        trainingLabels.slice(sliceTo(validationSize)),
                        testImages,
                        testLabels
                );
            }
            return new MnistDataset(trainingImages, trainingLabels, null, null, testImages, testLabels);

        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * @param trainingImagesArchive 训练图片路径
     * @param trainingLabelsArchive 训练标签路径
     * @param testImagesArchive     测试图片路径
     * @param testLabelsArchive     测试标签路径
     */
    public static MnistDataset getOneValidationImage(int index, String trainingImagesArchive, String trainingLabelsArchive,
                                                     String testImagesArchive, String testLabelsArchive) {
        try {
            ByteNdArray trainingImages = readArchive(trainingImagesArchive);
            ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
            ByteNdArray testImages = readArchive(testImagesArchive);
            ByteNdArray testLabels = readArchive(testLabelsArchive);
            trainingImages.slice(sliceFrom(0));
            trainingLabels.slice(sliceTo(0));
            // 切片操作
            Index range = Indices.range(index, index + 1);// 切片的起始和结束索引
            ByteNdArray validationImage = trainingImages.slice(range); // 执行切片操作
            ByteNdArray validationLable = trainingLabels.slice(range); // 执行切片操作


            if (index >= 0) {
                return new MnistDataset(
                        trainingImages,
                        trainingLabels,
                        validationImage,
                        validationLable,
                        testImages,
                        testLabels
                );
            } else {
                return null;
            }
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }

    private static ByteNdArray readArchive(String archiveName) throws IOException {
        System.out.println("archiveName = " + archiveName);
        DataInputStream archiveStream = new DataInputStream(
                //new GZIPInputStream(new java.io.FileInputStream("src/main/resources/"+archiveName))
                new GZIPInputStream(MnistDataset.class.getClassLoader().getResourceAsStream(archiveName))
        );
        //todo 不知道怎么读取和实际的内部结构
        archiveStream.readShort(); // first two bytes are always 0
        byte magic = archiveStream.readByte();
        if (magic != TYPE_UBYTE) {
            throw new IllegalArgumentException("\"" + archiveName + "\" is not a valid archive");
        }
        int numDims = archiveStream.readByte();
        long[] dimSizes = new long[numDims];
        int size = 1;  // for simplicity, we assume that total size does not exceeds Integer.MAX_VALUE
        for (int i = 0; i < dimSizes.length; ++i) {
            dimSizes[i] = archiveStream.readInt();
            size *= dimSizes[i];
        }
        byte[] bytes = new byte[size];
        archiveStream.readFully(bytes);
        return NdArrays.wrap(Shape.of(dimSizes), DataBuffers.of(bytes, false, false));
    }

    public Iterable<ImageBatch> trainingBatches(int batchSize) {
        return () -> new ImageBatchIterator(batchSize, trainingImages, trainingLabels);
    }

    public Iterable<ImageBatch> validationBatches(int batchSize) {
        return () -> new ImageBatchIterator(batchSize, validationImages, validationLabels);
    }

    public Iterable<ImageBatch> testBatches(int batchSize) {
        return () -> new ImageBatchIterator(batchSize, testImages, testLabels);
    }

    public ImageBatch testBatch() {
        return new ImageBatch(testImages, testLabels);
    }

    public long imageSize() {
        return imageSize;
    }

    public long numTrainingExamples() {
        return trainingLabels.shape().size(0);
    }

    public long numTestingExamples() {
        return testLabels.shape().size(0);
    }

    public long numValidationExamples() {
        return validationLabels.shape().size(0);
    }

    public ByteNdArray getValidationImages() {
        return validationImages;
    }

    public ByteNdArray getValidationLabels() {
        return validationLabels;
    }
}

SimpleMnist.class

package org.example.tensorDemo.dense;
import org.example.tensorDemo.datasets.ImageBatch;
import org.example.tensorDemo.datasets.mnist.MnistDataset;
import org.tensorflow.*;
import org.tensorflow.framework.optimizers.GradientDescent;
import org.tensorflow.framework.optimizers.Optimizer;
import org.tensorflow.ndarray.ByteNdArray;
import org.tensorflow.ndarray.Shape;
import org.tensorflow.op.Op;
import org.tensorflow.op.Ops;
import org.tensorflow.op.core.Placeholder;
import org.tensorflow.op.core.Variable;
import org.tensorflow.op.linalg.MatMul;
import org.tensorflow.op.math.Add;
import org.tensorflow.op.math.Mean;
import org.tensorflow.op.nn.Softmax;
import org.tensorflow.proto.framework.MetaGraphDef;
import org.tensorflow.proto.framework.SignatureDef;
import org.tensorflow.proto.framework.TensorInfo;
import org.tensorflow.types.TFloat32;
import org.tensorflow.types.TInt64;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;

public class SimpleMnist implements Runnable {
    private static final String TRAINING_IMAGES_ARCHIVE = "mnist/train-images-idx3-ubyte.gz";
    private static final String TRAINING_LABELS_ARCHIVE = "mnist/train-labels-idx1-ubyte.gz";
    private static final String TEST_IMAGES_ARCHIVE = "mnist/t10k-images-idx3-ubyte.gz";
    private static final String TEST_LABELS_ARCHIVE = "mnist/t10k-labels-idx1-ubyte.gz";

    public static void main(String[] args) {
        //加载数据集
//        MnistDataset dataset = MnistDataset.create(VALIDATION_SIZE, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,
//                TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);
        MnistDataset validationDataset = MnistDataset.getOneValidationImage(3, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,
                TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);
        //创建了一个名为graph的图形对象。
        try (Graph graph = new Graph()) {
            SimpleMnist mnist = new SimpleMnist(graph, validationDataset);
            mnist.run();//构建和训练模型
            mnist.loadModel("D:\\ai\\ai-demo");
        }
    }

    @Override
    public void run() {
        Ops tf = Ops.create(graph);
        // Create placeholders and variables, which should fit batches of an unknown number of images
        //创建占位符和变量,这些占位符和变量应适合未知数量的图像批次
        Placeholder<TFloat32> images = tf.placeholder(TFloat32.class);
        Placeholder<TFloat32> labels = tf.placeholder(TFloat32.class);

        // Create weights with an initial value of 0
        // 创建初始值为 0 的权重
        Shape weightShape = Shape.of(dataset.imageSize(), MnistDataset.NUM_CLASSES);
        Variable<TFloat32> weights = tf.variable(tf.zeros(tf.constant(weightShape), TFloat32.class));

        // Create biases with an initial value of 0
        //创建初始值为 0 的偏置
        Shape biasShape = Shape.of(MnistDataset.NUM_CLASSES);
        Variable<TFloat32> biases = tf.variable(tf.zeros(tf.constant(biasShape), TFloat32.class));

        // Predict the class of each image in the batch and compute the loss
        //使用 TensorFlow 的 tf.linalg.matMul 函数计算图像矩阵 images 和权重矩阵 weights 的矩阵乘法,并加上偏置项 biases。
        //wx+b
        MatMul<TFloat32> matMul = tf.linalg.matMul(images, weights);
        Add<TFloat32> add = tf.math.add(matMul, biases);

        //Softmax 是一个常用的激活函数,它将输入转换为表示概率分布的输出。对于输入向量中的每个元素,Softmax 函数会计算指数,
        //并对所有元素求和,然后将每个元素的指数除以总和,最终得到一个概率分布。这通常用于多分类问题,以输出每个类别的概率
        //激活函数 
        Softmax<TFloat32> softmax = tf.nn.softmax(add);

        // 创建一个计算交叉熵的Mean对象
        //损失函数
        Mean<TFloat32> crossEntropy =
                tf.math.mean(  // 计算张量的平均值
                        tf.math.neg(  // 计算张量的负值
                                tf.reduceSum(  // 计算张量的和
                                        tf.math.mul(labels, tf.math.log(softmax)),  //计算标签和softmax预测的对数乘积
                                        tf.array(1)  // 在指定轴上求和
                                )
                        ),
                        tf.array(0)  // 在指定轴上求平均值
                );

        // Back-propagate gradients to variables for training
        //使用梯度下降优化器来最小化交叉熵损失函数。首先,创建了一个梯度下降优化器 optimizer,然后使用该优化器来最小化交叉熵损失函数 crossEntropy。
        //梯度下降 https://www.cnblogs.com/guoyaohua/p/8542554.html
        Optimizer optimizer = new GradientDescent(graph, LEARNING_RATE);
        Op minimize = optimizer.minimize(crossEntropy);

        // Compute the accuracy of the model
        //使用 argMax 函数找出在给定轴上张量中最大值的索引,
        Operand<TInt64> predicted = tf.math.argMax(softmax, tf.constant(1));
        Operand<TInt64> expected = tf.math.argMax(labels, tf.constant(1));
        //使用 equal 函数比较模型预测的标签和实际标签是否相等,再用 cast 函数将布尔值转换为浮点数,最后使用 mean 函数计算准确率。
        Operand<TFloat32> accuracy = tf.math.mean(tf.dtypes.cast(tf.math.equal(predicted, expected), TFloat32.class), tf.array(0));

        // Run the graph
        try (Session session = new Session(graph)) {
            // Train the model
            for (ImageBatch trainingBatch : dataset.trainingBatches(TRAINING_BATCH_SIZE)) {
                try (TFloat32 batchImages = preprocessImages(trainingBatch.images());
                     TFloat32 batchLabels = preprocessLabels(trainingBatch.labels())) {
                    System.out.println("batchImages = " + batchImages.shape());
                    System.out.println("batchLabels = " + batchLabels.shape());
                    // 创建会话运行器
                    session.runner()
                            // 添加要最小化的目标
                            .addTarget(minimize)
                            // 通过feed方法将图像数据输入到模型中
                            .feed(images.asOutput(), batchImages)
                            // 通过feed方法将标签数据输入到模型中
                            .feed(labels.asOutput(), batchLabels)
                            // 运行会话
                            .run();
                }
            }

            // Test the model
            ImageBatch testBatch = dataset.testBatch();
            try (TFloat32 testImages = preprocessImages(testBatch.images());
                 TFloat32 testLabels = preprocessLabels(testBatch.labels());
                 // 定义一个TFloat32类型的变量accuracyValue,用于存储计算得到的准确率值
                 TFloat32 accuracyValue = (TFloat32) session.runner()
                         // 从会话中获取准确率值
                         .fetch(accuracy)
                         .fetch(predicted)
                         .fetch(expected)
                         // 将images作为输入,testImages作为数据进行喂养
                         .feed(images.asOutput(), testImages)
                         // 将labels作为输入,testLabels作为数据进行喂养
                         .feed(labels.asOutput(), testLabels)
                         // 运行会话并获取结果
                         .run()
                         // 获取第一个结果并存储在accuracyValue中
                         .get(0)) {
                System.out.println("Accuracy: " + accuracyValue.getFloat());
            }
            // 保存模型
            SavedModelBundle.Exporter exporter = SavedModelBundle.exporter("D:\\ai\\ai-demo").withSession(session);
            Signature.Builder builder = Signature.builder();
            builder.input("images", images);
            builder.input("labels", labels);
            builder.output("accuracy", accuracy);
            builder.output("expected", expected);
            builder.output("predicted", predicted);
            Signature signature = builder.build();
            SessionFunction sessionFunction = SessionFunction.create(signature, session);
            exporter.withFunction(sessionFunction);
            exporter.export();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private static final int VALIDATION_SIZE = 5;
    private static final int TRAINING_BATCH_SIZE = 100;
    private static final float LEARNING_RATE = 0.2f;

    private static TFloat32 preprocessImages(ByteNdArray rawImages) {
        Ops tf = Ops.create();
        // Flatten images in a single dimension and normalize their pixels as floats.
        long imageSize = rawImages.get(0).shape().size();
        return tf.math.div(
                tf.reshape(
                        tf.dtypes.cast(tf.constant(rawImages), TFloat32.class),
                        tf.array(-1L, imageSize)
                ),
                tf.constant(255.0f)
        ).asTensor();
    }

    private static TFloat32 preprocessLabels(ByteNdArray rawLabels) {
        Ops tf = Ops.create();
        // Map labels to one hot vectors where only the expected predictions as a value of 1.0
        return tf.oneHot(
                tf.constant(rawLabels),
                tf.constant(MnistDataset.NUM_CLASSES),
                tf.constant(1.0f),
                tf.constant(0.0f)
        ).asTensor();
    }

    private final Graph graph;
    private final MnistDataset dataset;

    private SimpleMnist(Graph graph, MnistDataset dataset) {
        this.graph = graph;
        this.dataset = dataset;
    }

    public void loadModel(String exportDir) {
        // load saved model
        SavedModelBundle model = SavedModelBundle.load(exportDir, "serve");
        try {
            printSignature(model);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        ByteNdArray validationImages = dataset.getValidationImages();
        ByteNdArray validationLabels = dataset.getValidationLabels();
        TFloat32 testImages = preprocessImages(validationImages);
        System.out.println("testImages = " + testImages.shape());
        TFloat32 testLabels = preprocessLabels(validationLabels);
        System.out.println("testLabels = " + testLabels.shape());
        Result run = model.session().runner()
                .feed("Placeholder:0", testImages)
                .feed("Placeholder_1:0", testLabels)
                .fetch("ArgMax:0")
                .fetch("ArgMax_1:0")
                .fetch("Mean_1:0")
                .run();
        // 处理输出
        Optional<Tensor> tensor1 = run.get("ArgMax:0");
        Optional<Tensor> tensor2 = run.get("ArgMax_1:0");
        Optional<Tensor> tensor3 = run.get("Mean_1:0");
        TInt64 predicted = (TInt64) tensor1.get();
        Long predictedValue = predicted.getObject(0);
        System.out.println("predictedValue = " + predictedValue);
        TInt64 expected = (TInt64) tensor2.get();
        Long expectedValue = expected.getObject(0);
        System.out.println("expectedValue = " + expectedValue);
        TFloat32 accuracy = (TFloat32) tensor3.get();
        System.out.println("accuracy = " + accuracy.getFloat());
    }

    private static void printSignature(SavedModelBundle model) throws Exception {
        MetaGraphDef m = model.metaGraphDef();
        SignatureDef sig = m.getSignatureDefOrThrow("serving_default");
        int numInputs = sig.getInputsCount();
        int i = 1;
        System.out.println("MODEL SIGNATURE");
        System.out.println("Inputs:");
        for (Map.Entry<String, TensorInfo> entry : sig.getInputsMap().entrySet()) {
            TensorInfo t = entry.getValue();
            System.out.printf(
                    "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
                    i++, numInputs, entry.getKey(), t.getName(), t.getDtype());
        }
        int numOutputs = sig.getOutputsCount();
        i = 1;
        System.out.println("Outputs:");
        for (Map.Entry<String, TensorInfo> entry : sig.getOutputsMap().entrySet()) {
            TensorInfo t = entry.getValue();
            System.out.printf(
                    "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
                    i++, numOutputs, entry.getKey(), t.getName(), t.getDtype());
        }
        System.out.println("-----------------------------------------------");
    }
}

五、最后两套代码运行结果





六、待完善点

1、这里并没有对提供web服务输入图片以及图片数据二值话等进行处理。有兴趣的小伙伴可以自己进行尝试

2、并没有使用卷积神经网络等,只是用了wx+b和激活函数进行跳跃,以及阶梯下降算法和交叉熵

3、没有进行更多层级的设计等

今年相关BBC纪录片播出后,旧杰尼斯事务所(现smile-up事务所,下文称“杰尼斯”)的创始人Johnny喜多川生前性侵未成年的Jr(年轻的杰尼斯偶像练习生)的丑闻获得广泛关注。最终,时任杰尼斯社长喜多川的外甥女藤岛景子承认喜多川性侵的事实并引咎辞职,杰尼斯也在继任者东山纪之手上解体。12月8日,旧杰尼斯公司成立新经纪公司STARTO ENTERTAINMENT,由新公司负责管理原杰尼斯艺人,福田淳就任社长。

令人惊讶的是,早在上世纪60年代,喜多川就曾因为涉嫌猥亵杰尼斯事务所艺能学校的学员遭到起诉。而在上世纪80年代,一些元杰尼斯偶像练习生曾在自己的著作中提到了喜多川的性侵行为。1988年元フォーリーブス(四叶草组合)的成员北公次在《To 光GENJIへ》一书中公开揭发喜多川对其实施性侵。此后一段时间内,其他前杰尼斯偶像也陆续站出来揭发喜多川的行为。

1999年,著名媒体《周刊文春》报道了喜多川性侵事件,并且获得多位被性侵的前杰尼斯偶像的证言,喜多川随即以诽谤罪将《周刊文春》起诉至东京高等法院。2000年日本众议院特别委员会讨论了这一问题,但未能通过有效的相关政策。2004年该案件经过二审,日本东京高等法院最终裁定文春报道的部分内容为真(包括喜多川猥亵少年的行为),即使是喜多川也无法否认这些偶像的证言的真实性,但最终法院要求文春向喜多川赔偿名誉损害费用,却未对喜多川提出指控或进行判决。日本媒体对这一事件反应极为冷淡,主流媒体均未大肆报道此事[1]。

在此之后,直到喜多川2019年去世,公众对此事都鲜有议论,杰尼斯事务所内的上层斗争——藤岛玛丽和饭岛三智、藤岛景子和泷泽秀明等人的斗争吸引了公众和粉丝的眼球。而SMAP、岚、TOKIO等组合的大火,将杰尼斯事务所的声望推到了新高,而之后的“偶像战国”时期几乎掩盖了这位创始人的罪恶。喜多川去世后,原来以及现在的杰尼斯偶像们(从中居正广到曾经在杰尼斯呆过的taka)均来到现场追悼,互联网上哀悼的声音占据了主流。直到2023年BBC的纪录片,才让人们重新发现了喜多川过去的恶行。

为什么多数媒体对喜多川的罪行不闻不问?除了文春等少数媒体一度对喜多川性侵事件进行深刻报道,并因此遭到起诉以外,各种日本媒体都选择回避这个问题。笔者认为,这与日本特殊的事务所制度,以及事务所和媒体的关系息息相关,正是杰尼斯事务所和日本各类媒体公司错综复杂的关系、双方的互利共生关系,以及事务所具有单向优势的特点,导致媒体在喜多川丑闻的报道上长期失声。

笔者8月在东京湾附近拍到的富士电视台大楼

电视媒体在杰尼斯事件中的态度

电视台是怎么经营的

在很长一段时间内,电视是日本人主要的娱乐方式。2015年NHK报告称有94.5%的日本人在周中看电视,全国平均每人每天观看电视3小时;哪怕在互联网快速发展的2020年,依然有87.2%的人收看电视,每人每天看电视平均时长超过2.5小时[2]。因此,电视台在喜多川事件中的回避态度,在一定程度上成为喜多川生前逃脱法律制裁的帮凶;如果电视台选择跟进报道喜多川的丑闻,可能会激发民众对此事的关注和追踪。那么,为什么电视台会在喜多川事件中缺位?该问题的源头自然是电视台和杰尼斯之间的利益共同体关系。

日本主流电视台包括一家国营电视台(NHK)和五家民营电视台(富士、TBS、东京台、NTV和朝日),其他电视台基本上都在这五大民营电视台的电视的放送网络中,因此民营电视台是日本电视节目的主力军。民营电视台需要盈利来维持经营,主要盈利策略包括媒体业务和不动产业务(都市开发),其中媒体业务是营业额大头,例如富士电视台2023年第二和第三季度营业额中77.1%是媒体业务,其他各类业务加在一起仅占总营业额的22.9%,上一个半年的营业额比例与之相似[3]。足见媒体业务在富士为代表的民营电视台营业额所占比例之高。

富士电视台2023年第二三季度营业额和利润报告

富士电视台2022年第四季度和2023年第一季度营业额和利润报告

对于民营电视台,媒体业务又可以分为广告业务、版权业务和配信收入业务等等,其中的收入大头是广告业务[4]。一般情况下,一家企业想要投放广告,需要经过若干步骤:想投放广告的赞助商将广告费交给广告代理商,广告代理商联系电视台花钱投放,电视台将广告需求告知广告制作公司并且付钱制作广告,最后将广告在自己电视台上投放。在这一过程中,电视台会抽走一定比例的广告制作费用,成为电视台的收入来源。

日本电视台收入方式(左边为NHK收入,右边为民放电视台收入)

广告代理商和杰尼斯的关系

在广告制作中,广告代理公司和电视台是关键的一环。日本的主要广告代理公司是电通和博报堂,前者是最大的广告代理公司,一定程度上把持着电视台的经济命脉,因此在日本非常具有影响力。例如著名女演员黑木瞳的丈夫是电通的副社长执行役员。2012年,她在青山学院中等学部上学的女儿被爆出联合同学霸凌一位女同学,最后自己被停课,但被害者也遭到停课,学校也否认有霸凌的存在。外界传言这件事和她父亲是电通高层,以及捐钱给学校有关[5]。考虑到青山学院中等学部算是日本诸多名人子女所在的学校,不会轻易被某些家长施加的压力所左右,可见在日本电通的地位不容小觑,并且对电视台有极大的影响力。

尽管电通和杰尼斯的关系缺少细节材料,但是纵观每年CM(广告)的起用数量,我们会发现杰尼斯偶像所占的比例不低,至多可以占据男性艺人榜单前列的1/3,巅峰期的岚几乎可以做到全员上榜(5人)[6][7][8][9][10]。可以证实的是,岚的成员,也是杰尼斯著名艺人樱井翔的父亲樱井俊既是邮电部门的官僚,还在2017-2022年间担任电通高层,一度官至代表取缔役副社长的位置。虽然没有直接证据证明樱井俊在杰尼斯和电通之间的联络发挥了多大的作用,但我们有理由推测电通在背后对杰尼斯的事业起到了助推作用。

电视台为什么选择杰尼斯艺人

不管电通是否直接影响了杰尼斯艺人的热度,但是从电通和电视台的角度来看,如今的杰尼斯艺人确实比诸多艺术家更能帮助企业赚钱。21世纪的日本社会的特点之一是审美观念的多元化。在上世纪70-80年代,某一电视节目收视率超过30%并不算一件稀奇事情,像NHK晨间剧《阿信》巅峰收视率甚至高达62%。到了上世纪90年代,日本电视剧收视率整体保持着高位,诸如《东京爱情故事》《同一屋檐下》《恋爱世纪》等经典日剧收视率能稳定在20%以上。

但进入21世纪,随着个人电脑和互联网的普及,日本电视剧的收视率开始逐渐下降。究其原因是在互联网时代,人人都可以主动参与讨论、分享个人观点,寻找认同来强化自己的审美;这大大改变了电视时代,观众只能通过线下和纸质媒体分享自己观点的情况。观点交流的时效性大大加强,观众也从被动接受到主动创造。电视收视率开始逐步下降,想制作出一部国民级的电视剧难度越来越大。日本论坛上曾经有一个共识,认为民营电视台电视剧收视率超过15%即可认为是实绩(爆款电视剧),根据统计我们可以看到,随着时间的推移,收视率超过15%的电视剧越来越少,到2021年后甚至已经不复存在[11]。另一方面,数据统计还显示,东京台以外四大民营电视台各个节目(包括电视剧和综艺)平均收视率都随着年份从10%-14%下降到8%-12%上下[12]。可以说,“好的电视节目”不一定能带来期待之中的受众,而且相比于创造好的IP和好的剧本之难,选择死忠粉丝足够多的艺人来主演电视剧或者主持节目就容易多了。选择粉丝多的艺人可以保证节目拥有以死忠粉为主的收视人数,死忠粉也愿意为了多看一眼偶像而看电视。因此,只要制作不要过于糟糕,这些节目的收视率下限仍然是有保障的。

2002-2023年电视剧实绩数量

2003-2023年电视节目平均收视率

对于广告代理商来说,他们对广告代言人和广告投放企业的选择自有其根据。对于电视台以及其投放时间的选择,很大程度上受到电视台收视率的影响——更高的收视率意味着更多的人在看电视,意味着可能有更多的潜在客户,这会强化电视台选择艺人出演的逻辑;另一方面,广告代理商本身也更倾向于选择死忠粉丝多的艺人作为代言人,这也和广告的逻辑息息相关。

广告在电视上获得成功,本质是因为电视在家庭空间内的亲密感。电视消费的背景意味着公众人物如家人一样亲密。电视公众人物形象在数量和质量上创造亲密感。电视通过特写镜头提供更大的亲密感,数字电视的清晰度和细节创造了一种逼真的表现,可以仔细阅读每一个面部细节[13]。电视拉近了观众和明星之间的距离,私人和公共融合形成了一个完全由艺人定义的空间。简言之,电视的亲密感将艺人的体验构建为一种媒介偷窥的形式。由于日本广告时间很短,较长的广告最多一分半,少的就几十秒,为了在短时间中实现情感认同的重要性,广告制作旨在将自发性和真实性的效果自然化[14]。

木村拓哉和广濑铃出演麦当劳广告,可见广告对二人的面部特写

在广告中,艺人不仅受到关注,而且激起欲望。明星的作用是吸引观众的注意力,而广告则依赖于明星的角色,通过渴望变得更接近或更像明星来塑造观众的行为。通过模仿和建模,表达了对更接近和形成联系的渴望。通过识别,艺人的形象成为粉丝获得快乐的来源。粉丝购买自己喜欢的艺人代言的商品本身就是表达对自己偶像的爱意、拉近和偶像距离,感受亲密感的一部分[15]。因此,死忠粉更容易被激发购买的热情,自然电视台更喜欢死忠粉丝多的艺人,而非那些知名度和地位很高,但粉丝死忠度不够、粉丝购买力不足的艺术家。

电视台和广告代理商对死忠粉丝多的艺人的强调完美符合杰尼斯偶像的特点。杰尼斯偶像以女性粉丝为主,他们最为吸引女粉丝的不是突出的实力,而是多变的形象。杰尼斯偶像的特点是缺乏深度,他们形象的生存状态可以用“空洞”来形容。然而,这只会增强粉丝无休止地(重新)创造和消费关于这些图标的个人叙事(或幻想)的倾向[16]。杰尼斯偶像具有某种真实性,并可能引发无尽的个人叙事,这取决于超越的空虚。在这里,“超越的空虚”意味着这些标志性人物的整个现实,而公众/粉丝永远无法获得“现实”,这使得粉丝们下意识地被吸引。任何深度感都会阻止粉丝们制作出关于偶像的“易于消费的叙事”,复杂性与杰尼斯偶像的吸引力无关,杰尼斯偶像的空虚(或缺乏自我意识和原则)为自己的粉丝提供了某种美学和想象力的满足。

除了形象上的空洞,杰尼斯大都以团队出道,他们的活跃使得粉丝对偶像的想象围绕成员在团队内的角色和形象。也许从女粉丝的(潜意识)角度出发,去理解杰尼斯偶像的超然空虚的关键,是对杰尼斯偶像的集体定义。团体的团结,而非成员的个性构建了最初在潜意识上吸引了女性粉丝的情境。这些粉丝希望对他们最喜欢的偶像进行多样化的叙述,因为每个偶像都是团体的一部分[17]。如今杰尼斯几乎没有直接以solo艺人出道的偶像,例如山下智久和龟梨和也等知名的个人偶像都曾或依旧有自己所属的团队(NEWS和KAT-TUN)。

杰尼斯偶像之所以能被想象,核心在于他们在外形上被视为shonen(男孩)。在日本的社会文化背景下,shonen投射出一种雌雄同体的感觉。理想化的shonen形象是杰尼斯偶像制作的核心,在女粉丝看来,shonen形象没有太多的性内涵,因此女性能幻想/创造杰尼斯偶像的理想化shonen形象。这表明,她们试图将偶像转变为幻想,而不是接受他们真实的男性身份[18]。这也是为什么很多“杰姨”(杰尼斯的女粉丝)无法接受喜多川曾实施性侵的事实,因为这破坏了shonen的形象,将性加在一个可以亲密想象的形象之上,因此她们会出于心理防御机制进行下意识的否定。

从杰尼斯演唱会的动员人数,不难发现杰尼斯偶像的死忠粉丝之多。日本巨蛋巡演(一次巡演中在五个巨蛋都召开演唱会)是歌手动员粉丝能力的体现,能开一次巨蛋巡演意味着死忠粉的数量惊人。历史上共有29位日本和外国艺人开过巨蛋巡演,其中杰尼斯就占据了5席[19](岚、SMAP、kinki kids、関ジャニ∞和Kis-My-Ft2)演唱会动员总人数超过1000万人的艺人一共有6位,杰尼斯占据4位(岚、SMAP、kinki kids和関ジャニ∞),演唱会动员总人数历史第一是岚[20]。从演唱会的动员能力,足见杰尼斯偶像的粉丝粘性,也难怪电视台和电通会更青睐杰尼斯偶像。

座无虚席的SMAP北京演唱会

日媒估计的潜在收视率可以印证杰尼斯艺人个人收视率水平[21]。潜在收视率是指无论节目制作如何,因艺人出演而增加的收视率。杰尼斯艺人的潜在收视率在日本娱乐圈艺人中排名比较靠前,尤其是有三位潜在收视率高于10%的演员,仅仅落后于两位国民男演员堺雅人和阿部宽。

根据日媒周刊现代披露的部分艺人潜在收视率数据统计做的前20排序

杰尼斯和日本事务所体制

实际上,日本娱乐圈本身就是事务所主导的体制。电视台一类的媒体公司在接触表演者方面对管理公司存在结构性依赖,对艺人外表和表现的决策权则完全掌握在事务所手中,因此媒体必须与管理公司协商访问权限。事务所几乎没有给艺人控制自己职业方向的自由,同时艺人几乎没有议价权。一旦艺人和事务所闹矛盾、尝试退出事务所,事务所会直接封杀艺人,禁止他们上电视或者转投其他事务所。

对于电视台而言,五大电视网之间的竞争以及这些公司限制接触艺人的能力意味着管理公司占据了上风:如果需求得不到满足,他们可能会威胁并表示将给其他电视台提供更好的待遇,所以电视台“不得不”按照大事务所的要求封杀艺人[22]。而如果不用杰尼斯偶像,换其他事务所的艺人,则包含了巨大的风险。富士台营业额和利润关系是个很好的例子,尽管媒体部门营业额占总营业额比重高达77%,但是利润却只占总利润的29%;即使在更好的情形下,媒体部门的利润率也不如实体利润率高。可见媒体部门盈利率低,稍有不慎就可能遭遇严重亏损。基于路径依赖,电视台会选择比较稳妥地继续和杰尼斯艺人合作,从而维持经营不亏损。至于立刻换掉杰尼斯艺人,那是无法想象的。在本次事件中,五大民营电视台中只有实力相对弱的东京电视台限制杰尼斯艺人出演东京台的综艺,其他四家电视台纷纷选择回避是否起用杰尼斯艺人的问题,或是维持当前起用艺人的方针。

杰尼斯对电视台的支配不仅体现在封杀草剪刚、香取慎吾等退社的杰尼斯艺人,甚至吉田羊和天海佑希这样的非杰尼斯演员也可能因为引起杰尼斯高层的不满而被限制和杰尼斯偶像共演。在日本,这种对电视台施压来封杀艺人的举措被称为“(电视台)忖度”。2023年9月7日,继任杰尼斯社长的东山纪之在电视上间接承认了以前忖度的存在(而在2019年各家电视台否认自己有过忖度),基本上坐实了杰尼斯对电视台的单方面支配[23]。这也不难理解为什么各家电视台为什么对喜多川的违法行径关注甚少。

当地时间2023年10月6日,日本东京,因受前任社长喜多川的性丑闻的影响,杰尼斯事务所港区大楼的公司招牌拆除工作完成。

纸质媒体的共谋

纸质媒体和杰尼斯的利益关系

喜多川性侵丑闻最早是由纸质媒体,例如文春和其他杂志报道的。但是他们没有坚持进行深入报道。在某些方面,纸媒和电视台面临同样的局面,家业庞大的杰尼斯事务所开始对媒体的施压。21世纪,纸质媒体整体呈现衰落趋势,并没有可以和杰尼斯抗衡的力量。除此之外,纸质媒体和电视台一样,并不完全是杰尼斯的敌人,相反还存在一定的共同利益。同时,这样的局面还和日本人的观念密切相关,即中产阶级文化和战败文化。

在2016年AKB48的纪录片《存在的理由》中,AKB48的工作人员专门去文春杂志的总部拜访文春报道娱乐圈绯闻的负责人。尽管文春曾多次报道AKB48的绯闻,甚至因此造成AKB48的动荡,但是从二者对话的态度,可见他们并不是想象的那样水火不容。实际上,偶像和小报都有着相同的利益追求,那就是尽可能向社会曝光自己。这使得二者容易成为事实上的利益相关:小报记者会报道一些重大绯闻获得曝光度,事务所有时候故意放出一些大新闻,有时候将无关痛痒的绯闻炒作,让全社会认识他们自己的艺人。例如2012年指原莉乃被文春爆出入团之前谈恋爱,这一期文春直接售罄。

而涉及到一些不利于创造共同利益的问题,这些杂志就可能会避而不谈,2019年NGT48山口真帆被霸凌事件期间,文春的报道就苍白无力,或顾左右而言他——这反过来证明杂志和事务所的利益相关性。就在杰尼斯新事务所STARTO ENTERTAINMENT成立当天,文春也专门采访了新社长福田淳,这是福田淳接受的第一次采访[24],可见文春和杰尼斯的关系并没有人们想象中那样差,双方仍然需要彼此来赚取关注度。

AKB48工作人员采访文春娱乐板块负责人(背对镜头者)

此外,日本媒体实际上已经有小报化的趋势。在酒井法子吸毒事件中,日本媒体的关注点似乎主要在酒井法子自己身上,通过产出既有共时性的,也有历时性的报道,并将其具体化为一个道德故事,将结构性因素(酒井法子的违法行为或多或少是由她的名人/家庭生活方式引起的)和个人失败混为一谈。小报式的高度主观、带着偏见的过度报道还加强了社会对边缘群体的偏见,从而规避了社会中存在的,却因禁忌而不被讨论的问题。酒井法子案引发了日本人对外国人犯罪的讨论,加深对外国人在东京市中心贩卖毒品的刻板印象,却忽略了日本本地黑社会组织深入参与毒品交易,并将活动外包给外国人的现实[25]。另外,在最近报道宝冢宙组成员自杀的过程中,文春的报道也会像小报一样编造故事。甚至喜多川事件中,文春之所以被判决赔偿杰尼斯事务所,也是因为他们像小报一样夸大其词,被杰尼斯抓住把柄,尽管这样的做法并未影响报道内容中的事实。

文春的行为反映出日本媒体杂志本身的小报化,其特点是从报道重要的社会问题转向八卦的信息娱乐和名人琐事。另外,也正是因为日本媒体对喜多川事件的回避,出现了沉默螺旋现象,希望进一步报道喜多川性侵丑闻的人因为看到被媒体放大的、漠不关心的声音,认为自己是少数,因此保持沉默。例如日本著名作曲家服部良一的儿子在被喜多川性侵后因为找不到支持者而沉默[26],音乐制作人松尾潔因为不满喜多川的行径被杰尼斯停止合作,自己也长期保持沉默[27]。

这种转变并非个例。赵鼎新曾经批评过西方主流媒体的保守性,经常跟着政府或所属党派转变自己的报道风口,而小报则和这些日本媒体一样侧重于报道新奇和激烈的新闻。这是因为发达国家存在着能为精英阶层以及大多数社会成员所认同的核心价值体系(即葛兰西式的霸权性文化),这个体系本质就是中产阶级价值体系[28],换言之,正是因为中产阶级的体量足够庞大,因此他们会更倾向于保守而不是对现实的激进改良。在市场的选择中,那些保守且符合他们观念的媒体,以及猎奇的小报留了下来。典型例子是以娱乐为主、面向中产阶级的《泰晤士报》凭借着广告收入降低自己的定价,最终压垮了发行量小,广告收入更少的左派报纸成为在英国大范围发行的媒体。在日本,日共的杂志虽然还在发行,但是因为定价高而且观点激进,导致其影响力远不如文春一类的杂志,除了少数中老年日共支持者以外,大部分年轻人对其完全不感兴趣。而日本人也对杰尼斯高层内斗兴趣十足,对喜多川性侵这种血淋淋的事实则采取回避态度,社会问题可比富豪们的小故事要更难以接受。

笔者8月在日本拍到的代代木日共总部

另外,对于时尚杂志而言,杰尼斯和他们的利益相关性也很强。如杰尼斯艺人适合广告一样,杰尼斯艺人也因为大量的死忠粉,进而能够提高杂志的销量。出版业的相关人士指出,在杂志封面不起用杰尼斯艺人之后,杂志销售额下降了近30%。所以各个时尚杂志没有抵制杰尼斯的动机,哪怕在杰尼斯“忖度”的时候,也希望能及时结束抵制杰尼斯。一位出版业的相关人士迫切希望杰尼斯新公司能尽快行动起来,他说:“如果继续这样下去,就有可能停刊或废刊了。”[29]

喜多川丑闻背后的战败文化

日本人对喜多川事件避而不谈的现象背后,还存在着心理因素,即被害者认知。喜多川的肆无忌惮,正是因为其他人知道却袖手旁观甚至参与其中(例如东山纪之)。在如今对喜多川清算的时候,诸如木村拓哉、东山纪之、樱井翔等在杰尼斯地位颇高的偶像,却对此事保持沉默或者回避评论喜多川的错误。社会上对冷漠的旁观者也有不少支持声音,笔者认为这和他们认为自己也是受害者有关。正是因为他们意识到喜多川行为是错误的,而自己袖手旁观也是不合理的,为了减轻自己心理上的压力,这些人选择将自己的形象转换为被害者而非加害者(毕竟袖手旁观实际上就是一种加害行为),从而减轻自己的心理压力。

在探究战后家庭应对黑历史和制造战后身份的策略时,德国心理学家引入了家庭相簿的概念,借指人们为家庭成员构建的正面形象,以此来防止负面家族历史被暴露。在这种保护性的动态关系之下,子女和孙辈用填补信息空白来抚平创口,强调了家庭成员在战争中遭受的苦难,以及他们的勇气和品德[30]。对于杰尼斯偶像和自认为有着亲密关系(有家人感)的粉丝而言,这些负罪的杰尼斯偶像也是喜多川压迫的受害者,是环境的受害者,是被迫袖手旁观的——换言之,他们是脆弱、无助的人,除了那么做,别无选择。

这种策略回避了偶像身上的道德责任,实际上是日本战后战败文化的反映。日本在二战中的战败使得他们发展出了自己独特的战败文化,在这个文化体系中,对二战的叙事提倡对失败战争中的悲惨受害者表示同情和认同。在这种叙事中,“灾难”的形象占了上风(一场规模空前的悲剧)强调了由残酷军事暴力所带来的全部残杀和破坏。这样的文化侧重于回避主要问题(为什么要打仗),仅仅一味地强调打仗不好,反而弱化了对军国主义的批判性。战败文化反映在喜多川事件之中,是对喜多川进行笼统地批判,而不再考虑相关人物的责任,以及如何预防此类事件的发生。尤其是没有形成道德责任感,没法在需要决断的时候承担自己的道德责任。

总结

韦伯曾经提出“理性的牢笼”来描述科层制在社会的扩展,科层制的合理性使其存在于政府、公司、学校等各种机构中。另一方面,科层制也对个体进行异化,使之去人格化。正是在这种世界中,媒体失声,人们面对杰尼斯这样的高塔放弃抵抗,保持沉默,放任事件的发生。很明显,不改变现在的事务所和媒体的共谋性,也不去深刻反思战败文化的不合理性,就很难对喜多川事件实现深刻的反思。

当然,互联网的存在,抢占了电视的资源,为Kpop等不同于传统日本本土审美的艺术风格提供了新的宣发渠道,从而冲击了事务所的地位;同时因为其匿名性,给更多的人表达观点的机会;女性主义通过互联网的进入也动摇了日本社会父权制的基础。综合以上的种种改变,我们有理由希望互联网在21世纪第三个十年给日本带来更大的变化,而一切的出发点,便是不再保持沉默。

参考文献

[1] 喜多川性侵案具体细节参考维基百科相关页面:https://ja.wikipedia.org/wiki/ジャニー喜多川による性加害問題

[2] 日本人观看电视的数据见NHK生活事件调查:https://www.nhk.or.jp/bunken/yoron-jikan/,

[3] 富士电视台的营业额和利润数据参见富士电视台自己的投资情报中间报告书:https://www.fujimediahd.co.jp/ir/l_report.html

[4] テレビ業界は赤字?ビジネスモデルや民放キー局今後の収益構造を解説https://matcher.jp/dictionary/articles/381

[5] 黒木瞳の娘の宝塚不合格…落ちる理由に驚愕https://www.xn--u9jy52gkffn9q8qbux6ab4xi9c4wsx57a.com/kurokihitomi-daughter-takarazuka

[6] 2022年TV-CMタレントランキングを発表https://mdata.tv/info/20221208_01/

[7] 2021年TV-CMタレントランキングを発表 https://mdata.tv/info/20211207_01/

[8] 2020年TV-CMタレントランキングを発表https://mdata.tv/info/20201204_01/

[9] 2019年TV-CMタレントランキングを発表https://mdata.tv/info/20191203_01/

[10] 2018年TV-CMタレントランキングを発表https://mdata.tv/info/20181204_01/

[11] 歴代ドラマ視聴率情報https://doraman.net/sp/index_rank/best30

[12] 各局とも下落続く…主要テレビ局の複数年にわたる視聴率推移(2023年12月公開版) https://news.yahoo.co.jp/expert/articles/25a5ae4a96219047e2f06446351e4a129c0437c9

[13] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:9

[14] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:77

[15] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:79

[16] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:99

[17] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:102

[18] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:104

[19] 简单科普190822为止,哪些艺人开过蛋巡?https://www.douban.com/group/topic/150290015/?_i=22176580NMbohA

[20] 部分歌手演唱会动员人数排名 https://www.douban.com/group/topic/295864875/?_i=22176060NMbohA

[21] 橋本環奈8.5%、阿部寛10.8%…極秘情報「潜在視聴率」を独占入手!ジャニーズ崩壊後、確実に「数字」を取れる「俳優の名前」 https://gendai.media/articles/-/117759?imp=0

[22] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:44

[23] 東山 メディアの忖度「必要ない」 退所タレントの活動は妨害しない https://www.sponichi.co.jp/entertainment/news/2023/09/08/kiji/20230908s00041000154000c.html

[24] 旧ジャニーズ「STARTO ENTERTAINMENT」福田淳新社長が真っ先に“週刊文春”の取材を受けた理由「性加害問題は…」〈独占告白150分〉https://bunshun.jp/articles/-/67542

[25] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:63

[26] 服部良一氏の78歳次男「ジャニー喜多川氏から幼少期に性被害受けた」…都内で会見 https://www.yomiuri.co.jp/culture/20230715-OYT1T50223/

[27] 松尾潔氏、山下達郎から離れるファンに呼びかけ「お考えを改める旨を表明したら…」https://www.nikkansports.com/entertainment/news/202308300000184.html

[28] 赵鼎新:《社会与政治运动讲义》,2012,北京:社会科学文献出版社.

[29] STARTO社設立に安堵する出版業界 非ジャニーズ表紙で「売り上げ落ちた」 https://www.tokyo-sports.co.jp/articles/-/285412

[30] 桥本明子:《漫长的战败:日本的文化创伤、记忆与认同》,李鹏程译,2021,上海:上海三联书店