{"id":174,"date":"2025-06-30T16:39:22","date_gmt":"2025-06-30T08:39:22","guid":{"rendered":"http:\/\/blog.sayoung.xyz\/?p=174"},"modified":"2026-02-10T15:27:55","modified_gmt":"2026-02-10T07:27:55","slug":"%e5%ad%a6%e4%b9%a0%e6%97%a5%e5%bf%97%e8%ae%ad%e7%bb%83%e4%b8%80%e4%b8%aa%e7%ae%80%e5%8d%95%e7%9a%84%e6%98%86%e8%99%ab%e8%af%86%e5%88%ab%e6%a8%a1%e5%9e%8b","status":"publish","type":"post","link":"https:\/\/blog.sayoung.wang\/index.php\/2025\/06\/30\/%e5%ad%a6%e4%b9%a0%e6%97%a5%e5%bf%97%e8%ae%ad%e7%bb%83%e4%b8%80%e4%b8%aa%e7%ae%80%e5%8d%95%e7%9a%84%e6%98%86%e8%99%ab%e8%af%86%e5%88%ab%e6%a8%a1%e5%9e%8b\/","title":{"rendered":"\u5b66\u4e60\u65e5\u5fd7|\u8bad\u7ec3\u4e00\u4e2a\u7b80\u5355\u7684\u6606\u866b\u8bc6\u522b\u6a21\u578b"},"content":{"rendered":"\n<div class=\"wp-block-argon-alert alert\" style=\"background-color:#7889e8\"><span class=\"alert-inner--icon\"><i class=\"fa fa-info-circle\"><\/i><\/span><span class=\"alert-inner--text\"><strong>\u4ec5\u8bb0\u5f55\u81ea\u8eab\u5b66\u4e60\u8fc7\u7a0b\uff0c\u4e0d\u9002\u5408\u4f5c\u4e3a\u6559\u7a0b\u6d4f\u89c8<\/strong><\/span><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">\u6570\u636e\u641c\u96c6<\/h2>\n\n\n\n<p>\u5728Kaggle\uff08<a href=\"https:\/\/www.kaggle.com\"><a href=\"https:\/\/www.kaggle.com\/datasets\">Find Open Datasets and Machine Learning Projects | Kaggle<\/a><\/a>\uff09\u7b49\u7f51\u7ad9\u4e0a\u4e0b\u8f7d\u6570\u636e\u96c6\uff0c\u6216\u662f\u81ea\u884c\u7528\u722c\u866b\u722c\u53d6<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u6570\u636e\u6e05\u6d17<\/h2>\n\n\n\n<p>\u4e0b\u8f7d\u4e0b\u6765\u7684\u6570\u636e\u5f80\u5f80\u5b58\u5728\u5927\u91cf\u91cd\u590d\u6216\u4e0d\u7b26\u5408\u8981\u6c42\u7684\u4f4e\u8d28\/\u4e3b\u9898\u65e0\u5173\u56fe\u50cf\uff0c\u9700\u8981\u8fdb\u884c\u4e00\u5b9a\u5904\u7406\u624d\u53ef\u7528\u4e8e\u8bad\u7ec3\u3002\u7531\u4e8e\u6b64\u6b21\u76ee\u7684\u4ec5\u5728\u4e8e\u8dd1\u901a\u6574\u4e2a\u8bad\u7ec3\u6d41\u7a0b\uff0c\u56e0\u6b64\u76f4\u63a5\u4f7f\u7528\u6574\u7406\u597d\u7684\u6570\u636e\u96c6\uff0c\u4ec5\u8fdb\u884c\u67e5\u91cd\u3002<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>deepseek\u5199\u7684\u7528\u4e8e\u67e5\u91cd\u7684\u7a97\u4f53\u7a0b\u5e8f<\/summary>\n<p><\/p>\n\n\n\n<p>\u5e76\u4e0d\u597d\u7528\uff0c\u56fe\u7247\u4e00\u591a\u5c31\u7279\u522b\u5361\u3002\u672c\u6765\u60f3\u5b8c\u5168\u4f9d\u9760deepseek\u751f\u6210\u7684\uff0c\u4f46\u6700\u540e\u8fd8\u662f\u5f97\u81ea\u5df1\u4e0b\u573a\u4feebug\u3002<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport sys\nimport os\nimport shutil\nfrom collections import defaultdict\nfrom PIL import Image\nfrom PyQt5.QtWidgets import (\n    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,\n    QFileDialog, QListWidget, QListWidgetItem, QCheckBox, QScrollArea, QGroupBox,\n    QDialog, QGridLayout, QMessageBox, QProgressBar\n)\nfrom PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal\nfrom PyQt5.QtGui import QPixmap, QIcon, QFont, QImage, QPainter\nfrom imagededup.methods import PHash\n\nclass ImageViewerDialog(QDialog):\n    &quot;&quot;&quot;\u56fe\u7247\u67e5\u770b\u5bf9\u8bdd\u6846\uff0c\u663e\u793a\u5927\u56fe&quot;&quot;&quot;\n    def __init__(self, image_path, parent=None):\n        super().__init__(parent)\n        self.setWindowTitle(&quot;\u56fe\u7247\u67e5\u770b\u5668&quot;)\n        self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint)\n        self.setMinimumSize(800, 600)\n        \n        layout = QVBoxLayout()\n        \n        # \u663e\u793a\u56fe\u7247\u8def\u5f84\n        path_label = QLabel(f&quot;\u56fe\u7247\u8def\u5f84: {image_path}&quot;)\n        path_label.setWordWrap(True)\n        path_label.setStyleSheet(&quot;font-size: 12px; color: #666;&quot;)\n        layout.addWidget(path_label)\n        \n        # \u663e\u793a\u56fe\u7247\n        self.image_label = QLabel()\n        self.image_label.setAlignment(Qt.AlignCenter)\n        self.image_label.setStyleSheet(&quot;background-color: #f0f0f0;&quot;)\n        \n        scroll_area = QScrollArea()\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setWidget(self.image_label)\n        layout.addWidget(scroll_area)\n        \n        # \u5173\u95ed\u6309\u94ae\n        close_btn = QPushButton(&quot;\u5173\u95ed&quot;)\n        close_btn.setStyleSheet(\n            &quot;QPushButton { background-color: #4CAF50; color: white; padding: 8px 16px; border-radius: 4px; }&quot;\n            &quot;QPushButton:hover { background-color: #45a049; }&quot;\n        )\n        close_btn.clicked.connect(self.close)\n        layout.addWidget(close_btn, alignment=Qt.AlignCenter)\n        \n        self.setLayout(layout)\n        self.load_image(image_path)\n    \n    def load_image(self, image_path):\n        &quot;&quot;&quot;\u52a0\u8f7d\u5e76\u663e\u793a\u56fe\u7247\uff08\u4fee\u590d\u663e\u793a\u95ee\u9898\uff09&quot;&quot;&quot;\n        try:\n            # \u52a0\u8f7d\u56fe\u50cf\u5e76\u8f6c\u6362\u4e3aRGBA\u6a21\u5f0f\n            image = Image.open(image_path).convert(&quot;RGBA&quot;)\n            \n            # \u9650\u5236\u6700\u5927\u663e\u793a\u5c3a\u5bf8\n            max_size = QSize(1600, 1200)\n            if image.width &gt; max_size.width() or image.height &gt; max_size.height():\n                image.thumbnail((max_size.width(), max_size.height()), Image.LANCZOS)\n            \n            # \u521b\u5efaQImage\u5e76\u786e\u4fdd\u6b63\u786e\u7684\u683c\u5f0f\n            qimage = QImage(\n                image.tobytes(), \n                image.width, \n                image.height, \n                QImage.Format_RGBA8888\n            )\n            \n            pixmap = QPixmap.fromImage(qimage)\n            self.image_label.setPixmap(pixmap)\n        except Exception as e:\n            self.image_label.setText(f&quot;\u65e0\u6cd5\u52a0\u8f7d\u56fe\u7247: {str(e)}&quot;)\n\nclass DedupWorker(QThread):\n    &quot;&quot;&quot;\u540e\u53f0\u7ebf\u7a0b\u5904\u7406\u56fe\u7247\u67e5\u91cd&quot;&quot;&quot;\n    progress_updated = pyqtSignal(int, int, str)  # \u5f53\u524d\u8fdb\u5ea6, \u603b\u6570, \u5f53\u524d\u6587\u4ef6\n    duplicates_found = pyqtSignal(dict, list)     # \u91cd\u590d\u7ec4, \u552f\u4e00\u56fe\u7247\n    \n    def __init__(self, image_dir):\n        super().__init__()\n        self.image_dir = image_dir\n        self.canceled = False\n    \n    def run(self):\n        &quot;&quot;&quot;\u6267\u884c\u67e5\u91cd\u64cd\u4f5c\uff08\u4fee\u590d\u552f\u4e00\u56fe\u7247\u8ba1\u6570\uff09&quot;&quot;&quot;\n        try:\n            # \u83b7\u53d6\u6240\u6709\u56fe\u7247\u6587\u4ef6\n            image_files = &#x5B;]\n            extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp')\n            for root, _, files in os.walk(self.image_dir):\n                for file in files:\n                    if file.lower().endswith(extensions):\n                        image_files.append(os.path.join(root, file))\n            \n            if not image_files:\n                self.progress_updated.emit(0, 0, &quot;\u672a\u627e\u5230\u56fe\u7247\u6587\u4ef6&quot;)\n                return\n            \n            total = len(image_files)\n            self.progress_updated.emit(0, total, &quot;\u5f00\u59cb\u5904\u7406...&quot;)\n            \n            # \u4f7f\u7528PHash\u7b97\u6cd5\u8fdb\u884c\u67e5\u91cd\n            phasher = PHash()\n            encodings = {}\n            \n            # \u8ba1\u7b97\u56fe\u7247\u54c8\u5e0c\n            for i, image_path in enumerate(image_files, 1):\n                if self.canceled:\n                    return\n                \n                self.progress_updated.emit(i, total, os.path.basename(image_path))\n                try:\n                    encodings&#x5B;image_path] = phasher.encode_image(image_path)\n                except Exception as e:\n                    print(f&quot;\u5904\u7406\u56fe\u7247 {image_path} \u65f6\u51fa\u9519: {str(e)}&quot;)\n            \n            # \u67e5\u627e\u91cd\u590d\u56fe\u7247\n            duplicates = phasher.find_duplicates(encoding_map=encodings)\n            \n            # \u7ec4\u7ec7\u91cd\u590d\u7ec4\n            grouped_duplicates = defaultdict(list)\n            visited = set()\n            dup_group =&#x5B;] #\u7edf\u8ba1\u6240\u6709\u91cd\u590d\u56fe\u7247\n            for img, dup_list in duplicates.items():\n                if img in visited:\n                    continue\n                \n                # \u521b\u5efa\u4e00\u4e2a\u65b0\u7684\u91cd\u590d\u7ec4\n                group = &#x5B;img]\n                visited.add(img)\n                \n                # \u6dfb\u52a0\u6240\u6709\u91cd\u590d\u9879\n                for dup in dup_list:\n                    if dup not in visited:\n                        group.append(dup)\n                        visited.add(dup)\n                \n                # \u6dfb\u52a0\u7ec4\n                if len(group) &gt; 1: #group\u957f\u5ea6\u5927\u4e8e\u4e00\uff0c\u5373\u5b58\u5728\u91cd\u590d\u56fe\u7247\n                    grouped_duplicates&#x5B;img] = group\n                    dup_group.extend(group) #\u6dfb\u52a0\u91cd\u590d\u56fe\u7247\n            \n            # \u83b7\u53d6\u552f\u4e00\u56fe\u7247 - \u4fee\u590d\u552f\u4e00\u56fe\u7247\u8ba1\u6570\u95ee\u9898\n            unique_images = &#x5B;img for img in image_files if img not in dup_group]#\u539f\u6765\u662funique_images = &#x5B;img for img in image_files if img not in visited]\n            print(&quot;nique_images:&quot;,len(unique_images))\n            \n            self.duplicates_found.emit(grouped_duplicates, unique_images)\n        \n        except Exception as e:\n            print(f&quot;\u67e5\u91cd\u8fc7\u7a0b\u4e2d\u53d1\u751f\u9519\u8bef: {str(e)}&quot;)\n    \n    def cancel(self):\n        &quot;&quot;&quot;\u53d6\u6d88\u64cd\u4f5c&quot;&quot;&quot;\n        self.canceled = True\n\nclass ImageDedupTool(QMainWindow):\n    &quot;&quot;&quot;\u56fe\u7247\u67e5\u91cd\u5de5\u5177\u4e3b\u754c\u9762\uff08\u4fee\u590d\u6240\u6709\u62a5\u544a\u7684\u95ee\u9898\uff09&quot;&quot;&quot;\n    def __init__(self):\n        super().__init__()\n        self.setWindowTitle(&quot;\u56fe\u7247\u67e5\u91cd\u5de5\u5177&quot;)\n        self.setGeometry(100, 100, 1000, 700)\n        self.setWindowIcon(QIcon(self.create_icon()))\n        \n        # \u521d\u59cb\u5316\u53d8\u91cf\n        self.image_dir = &quot;&quot;\n        self.output_dir = &quot;&quot;\n        self.duplicate_groups = {}\n        self.unique_images = &#x5B;]\n        self.selected_images = set()\n        self.worker_thread = None\n        \n        # \u521b\u5efa\u4e3b\u90e8\u4ef6\u548c\u5e03\u5c40\n        main_widget = QWidget()\n        main_layout = QVBoxLayout()\n        \n        # \u6807\u9898\n        title_label = QLabel(&quot;\u56fe\u7247\u67e5\u91cd\u5de5\u5177&quot;)\n        title_label.setFont(QFont(&quot;Arial&quot;, 18, QFont.Bold))\n        title_label.setStyleSheet(&quot;color: #2c3e50; margin: 15px 0;&quot;)\n        title_label.setAlignment(Qt.AlignCenter)\n        main_layout.addWidget(title_label)\n        \n        # \u6587\u4ef6\u5939\u9009\u62e9\u533a\u57df\n        folder_group = QGroupBox(&quot;\u6587\u4ef6\u5939\u8bbe\u7f6e&quot;)\n        folder_layout = QGridLayout()\n        \n        self.image_dir_label = QLabel(&quot;\u672a\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939&quot;)\n        self.image_dir_label.setStyleSheet(&quot;font-size: 12px;&quot;)\n        self.image_dir_label.setWordWrap(True)\n        folder_layout.addWidget(QLabel(&quot;\u56fe\u7247\u6587\u4ef6\u5939:&quot;), 0, 0)\n        folder_layout.addWidget(self.image_dir_label, 0, 1)\n        \n        self.output_dir_label = QLabel(&quot;\u672a\u9009\u62e9\u8f93\u51fa\u6587\u4ef6\u5939&quot;)\n        self.output_dir_label.setStyleSheet(&quot;font-size: 12px;&quot;)\n        self.output_dir_label.setWordWrap(True)\n        folder_layout.addWidget(QLabel(&quot;\u8f93\u51fa\u6587\u4ef6\u5939:&quot;), 1, 0)\n        folder_layout.addWidget(self.output_dir_label, 1, 1)\n        \n        self.select_image_btn = QPushButton(&quot;\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939&quot;)\n        self.select_image_btn.setStyleSheet(self.get_button_style())\n        self.select_image_btn.clicked.connect(self.select_image_dir)\n        folder_layout.addWidget(self.select_image_btn, 0, 2)\n        \n        self.select_output_btn = QPushButton(&quot;\u9009\u62e9\u8f93\u51fa\u6587\u4ef6\u5939&quot;)\n        self.select_output_btn.setStyleSheet(self.get_button_style())\n        self.select_output_btn.clicked.connect(self.select_output_dir)\n        folder_layout.addWidget(self.select_output_btn, 1, 2)\n        \n        folder_group.setLayout(folder_layout)\n        main_layout.addWidget(folder_group)\n        \n        # \u64cd\u4f5c\u6309\u94ae\u533a\u57df\n        btn_layout = QHBoxLayout()\n        \n        self.start_btn = QPushButton(&quot;\u5f00\u59cb\u67e5\u91cd&quot;)\n        self.start_btn.setStyleSheet(self.get_button_style(&quot;#3498db&quot;))\n        self.start_btn.clicked.connect(self.start_dedup)\n        self.start_btn.setEnabled(False)\n        btn_layout.addWidget(self.start_btn)\n        \n        self.cancel_btn = QPushButton(&quot;\u53d6\u6d88&quot;)\n        self.cancel_btn.setStyleSheet(self.get_button_style(&quot;#e74c3c&quot;))\n        self.cancel_btn.clicked.connect(self.cancel_dedup)\n        self.cancel_btn.setEnabled(False)\n        btn_layout.addWidget(self.cancel_btn)\n        \n        self.export_btn = QPushButton(&quot;\u5bfc\u51fa\u7ed3\u679c&quot;)\n        self.export_btn.setStyleSheet(self.get_button_style(&quot;#2ecc71&quot;))\n        self.export_btn.clicked.connect(self.export_results)\n        self.export_btn.setEnabled(False)\n        btn_layout.addWidget(self.export_btn)\n        \n        main_layout.addLayout(btn_layout)\n        \n        # \u8fdb\u5ea6\u6761\n        self.progress_bar = QProgressBar()\n        self.progress_bar.setStyleSheet(\n            &quot;QProgressBar { border: 1px solid #3498db; border-radius: 5px; text-align: center; }&quot;\n            &quot;QProgressBar::chunk { background-color: #3498db; }&quot;\n        )\n        self.progress_bar.setVisible(False)\n        main_layout.addWidget(self.progress_bar)\n        \n        # \u7ed3\u679c\u533a\u57df\n        result_group = QGroupBox(&quot;\u67e5\u91cd\u7ed3\u679c&quot;)\n        result_layout = QVBoxLayout()\n        \n        # \u7ed3\u679c\u7edf\u8ba1\u6807\u7b7e\n        self.result_label = QLabel(&quot;\u8bf7\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939\u5e76\u70b9\u51fb'\u5f00\u59cb\u67e5\u91cd'\u6309\u94ae\u7ee7\u7eed&quot;)\n        self.result_label.setStyleSheet(&quot;font-size: 14px; margin: 10px 0;&quot;)\n        self.result_label.setAlignment(Qt.AlignCenter)\n        result_layout.addWidget(self.result_label)\n        \n        # \u7ed3\u679c\u5217\u8868\u533a\u57df\n        self.result_scroll = QScrollArea()\n        self.result_scroll.setWidgetResizable(True)\n        \n        self.result_container = QWidget()\n        self.result_container_layout = QVBoxLayout()\n        self.result_container_layout.setAlignment(Qt.AlignTop)\n        \n        self.result_container.setLayout(self.result_container_layout)\n        self.result_scroll.setWidget(self.result_container)\n        \n        result_layout.addWidget(self.result_scroll)\n        result_group.setLayout(result_layout)\n        main_layout.addWidget(result_group, 1)\n        \n        main_widget.setLayout(main_layout)\n        self.setCentralWidget(main_widget)\n        \n        # \u72b6\u6001\u680f\n        self.statusBar().showMessage(&quot;\u5c31\u7eea&quot;)\n    \n    def create_icon(self):\n        &quot;&quot;&quot;\u521b\u5efa\u5e94\u7528\u56fe\u6807&quot;&quot;&quot;\n        # \u4f7f\u7528\u7b80\u5355\u56fe\u6807\u66ff\u4ee3\n        pixmap = QPixmap(64, 64)\n        pixmap.fill(Qt.transparent)\n        return QIcon(pixmap)\n    \n    def get_button_style(self, color=&quot;#7f8c8d&quot;):\n        &quot;&quot;&quot;\u83b7\u53d6\u6309\u94ae\u6837\u5f0f&quot;&quot;&quot;\n        return f&quot;&quot;&quot;\n            QPushButton {{\n                background-color: {color};\n                color: white;\n                border: none;\n                padding: 8px 16px;\n                font-size: 14px;\n                border-radius: 4px;\n            }}\n            QPushButton:hover {{\n                background-color: #{int(color&#x5B;1:], 16) - 0x111111:06X};\n            }}\n            QPushButton:disabled {{\n                background-color: #bdc3c7;\n            }}\n        &quot;&quot;&quot;\n    \n    def select_image_dir(self):\n        &quot;&quot;&quot;\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939&quot;&quot;&quot;\n        dir_path = QFileDialog.getExistingDirectory(self, &quot;\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939&quot;)\n        if dir_path:\n            self.image_dir = dir_path\n            self.image_dir_label.setText(dir_path)\n            self.start_btn.setEnabled(True)\n            self.result_label.setText(&quot;\u5df2\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939\uff0c\u70b9\u51fb'\u5f00\u59cb\u67e5\u91cd'\u6309\u94ae\u7ee7\u7eed&quot;)\n    \n    def select_output_dir(self):\n        &quot;&quot;&quot;\u9009\u62e9\u8f93\u51fa\u6587\u4ef6\u5939&quot;&quot;&quot;\n        dir_path = QFileDialog.getExistingDirectory(self, &quot;\u9009\u62e9\u8f93\u51fa\u6587\u4ef6\u5939&quot;)\n        if dir_path:\n            self.output_dir = dir_path\n            self.output_dir_label.setText(dir_path)\n    \n    def start_dedup(self):\n        &quot;&quot;&quot;\u5f00\u59cb\u67e5\u91cd\u64cd\u4f5c&quot;&quot;&quot;\n        if not self.image_dir:\n            QMessageBox.warning(self, &quot;\u8b66\u544a&quot;, &quot;\u8bf7\u5148\u9009\u62e9\u56fe\u7247\u6587\u4ef6\u5939&quot;)\n            return\n        \n        # \u6e05\u7a7a\u4e4b\u524d\u7684\u7ed3\u679c\n        self.duplicate_groups = {}\n        self.unique_images = &#x5B;]\n        self.selected_images = set()\n        self.clear_result_container()\n        \n        # \u663e\u793a\u8fdb\u5ea6\u6761\n        self.progress_bar.setVisible(True)\n        self.progress_bar.setValue(0)\n        self.result_label.setText(&quot;\u6b63\u5728\u5904\u7406\u56fe\u7247\uff0c\u8bf7\u7a0d\u5019...&quot;)\n        self.statusBar().showMessage(&quot;\u6b63\u5728\u67e5\u627e\u91cd\u590d\u56fe\u7247...&quot;)\n        \n        # \u7981\u7528\u6309\u94ae\n        self.start_btn.setEnabled(False)\n        self.cancel_btn.setEnabled(True)\n        self.export_btn.setEnabled(False)\n        \n        # \u542f\u52a8\u540e\u53f0\u7ebf\u7a0b\n        self.worker_thread = DedupWorker(self.image_dir)\n        self.worker_thread.progress_updated.connect(self.update_progress)\n        self.worker_thread.duplicates_found.connect(self.process_results)\n        self.worker_thread.start()\n    \n    def cancel_dedup(self):\n        &quot;&quot;&quot;\u53d6\u6d88\u67e5\u91cd\u64cd\u4f5c&quot;&quot;&quot;\n        if self.worker_thread and self.worker_thread.isRunning():\n            self.worker_thread.cancel()\n            self.worker_thread.wait()\n        \n        self.progress_bar.setVisible(False)\n        self.result_label.setText(&quot;\u64cd\u4f5c\u5df2\u53d6\u6d88&quot;)\n        self.statusBar().showMessage(&quot;\u64cd\u4f5c\u5df2\u53d6\u6d88&quot;)\n        \n        # \u6062\u590d\u6309\u94ae\u72b6\u6001\n        self.start_btn.setEnabled(True)\n        self.cancel_btn.setEnabled(False)\n    \n    def update_progress(self, current, total, filename):\n        &quot;&quot;&quot;\u66f4\u65b0\u8fdb\u5ea6\u6761&quot;&quot;&quot;\n        if total &gt; 0:\n            progress = int((current \/ total) * 100)\n            self.progress_bar.setValue(progress)\n            self.statusBar().showMessage(f&quot;\u6b63\u5728\u5904\u7406: {filename} ({current}\/{total})&quot;)\n    \n    def process_results(self, duplicates, uniques):\n        &quot;&quot;&quot;\u5904\u7406\u67e5\u91cd\u7ed3\u679c&quot;&quot;&quot;\n        self.duplicate_groups = duplicates\n        self.unique_images = uniques\n        self.selected_images = set(self.unique_images)  # \u81ea\u52a8\u9009\u62e9\u552f\u4e00\u56fe\u7247\n        \n        # \u5bf9\u4e8e\u91cd\u590d\u7ec4\uff0c\u9ed8\u8ba4\u9009\u62e9\u6bcf\u7ec4\u7684\u7b2c\u4e00\u5f20\u56fe\u7247\n        for group in self.duplicate_groups.values():\n            if group:  # \u786e\u4fdd\u7ec4\u4e0d\u4e3a\u7a7a\n                self.selected_images.add(group&#x5B;0])\n        \n        # \u9690\u85cf\u8fdb\u5ea6\u6761\n        self.progress_bar.setVisible(False)\n        \n        # \u66f4\u65b0\u7ed3\u679c\u7edf\u8ba1\n        duplicate_count = sum(len(group) for group in self.duplicate_groups.values())\n        unique_count = len(self.unique_images)\n        total_images = duplicate_count + unique_count\n        \n        self.result_label.setText(\n            f&quot;\u67e5\u91cd\u5b8c\u6210\uff01\u5171\u53d1\u73b0 {len(self.duplicate_groups)} \u7ec4\u91cd\u590d\u56fe\u7247 &quot;\n            f&quot;({duplicate_count} \u5f20), {unique_count} \u5f20\u552f\u4e00\u56fe\u7247&quot;\n        )\n        self.statusBar().showMessage(&quot;\u67e5\u91cd\u5b8c\u6210&quot;)\n        \n        # \u663e\u793a\u7ed3\u679c\n        self.display_results()\n        \n        # \u66f4\u65b0\u6309\u94ae\u72b6\u6001\n        self.start_btn.setEnabled(True)\n        self.cancel_btn.setEnabled(False)\n        self.export_btn.setEnabled(True)\n    \n    def clear_result_container(self):\n        &quot;&quot;&quot;\u6e05\u7a7a\u7ed3\u679c\u5bb9\u5668&quot;&quot;&quot;\n        while self.result_container_layout.count():\n            child = self.result_container_layout.takeAt(0)\n            if child.widget():\n                child.widget().deleteLater()\n    \n    def display_results(self):\n        &quot;&quot;&quot;\u663e\u793a\u67e5\u91cd\u7ed3\u679c&quot;&quot;&quot;\n        self.clear_result_container()\n               \n        # \u663e\u793a\u91cd\u590d\u56fe\u7247\u7ec4\n        for group_idx, (_, group) in enumerate(self.duplicate_groups.items(), 1):\n            group_box = QGroupBox(f&quot;\u91cd\u590d\u56fe\u7247\u7ec4 {group_idx} - \u5171 {len(group)} \u5f20&quot;)\n            group_layout = QVBoxLayout()\n            \n            for img_path in group:\n                img_item = self.create_image_item(img_path, group_idx)\n                group_layout.addWidget(img_item)\n            \n            group_box.setLayout(group_layout)\n            self.result_container_layout.addWidget(group_box)\n\n        # \u663e\u793a\u552f\u4e00\u56fe\u7247\n        if self.unique_images:\n            unique_group = QGroupBox(f&quot;\u552f\u4e00\u56fe\u7247 - \u5171 {len(self.unique_images)} \u5f20&quot;)\n            unique_layout = QVBoxLayout()\n            \n            for img_path in self.unique_images:\n                img_item = self.create_image_item(img_path, is_unique=True)\n                unique_layout.addWidget(img_item)\n            \n            unique_group.setLayout(unique_layout)\n            self.result_container_layout.addWidget(unique_group)\n        \n    def create_image_item(self, image_path, group_id=None, is_unique=False):\n        &quot;&quot;&quot;\u521b\u5efa\u56fe\u7247\u9879\u63a7\u4ef6\uff08\u4fee\u590d\u9009\u62e9\u95ee\u9898\u548c\u7f29\u7565\u56fe\u95ee\u9898\uff09&quot;&quot;&quot;\n        item_widget = QWidget()\n        item_layout = QHBoxLayout()\n        \n        # \u7f29\u7565\u56fe\n        try:\n            # \u52a0\u8f7d\u56fe\u50cf\u5e76\u786e\u4fdd\u4f7f\u7528RGBA\u683c\u5f0f\n            img = Image.open(image_path).convert(&quot;RGBA&quot;)\n            img.thumbnail((120, 120), Image.LANCZOS)\n            \n            # \u521b\u5efaQImage\u5e76\u786e\u4fdd\u6b63\u786e\u7684\u683c\u5f0f\n            qimage = QImage(\n                img.tobytes(), \n                img.width, \n                img.height, \n                QImage.Format_RGBA8888\n            )\n            \n            pixmap = QPixmap.fromImage(qimage)\n            \n            thumbnail_label = QLabel()\n            thumbnail_label.setPixmap(pixmap)\n            thumbnail_label.setStyleSheet(&quot;border: 1px solid #ddd; padding: 5px;&quot;)\n            thumbnail_label.setCursor(Qt.PointingHandCursor)\n            thumbnail_label.mousePressEvent = lambda e: self.show_image(image_path)\n        except Exception as e:\n            print(f&quot;\u521b\u5efa\u7f29\u7565\u56fe\u65f6\u51fa\u9519: {str(e)}&quot;)\n            thumbnail_label = QLabel(&quot;\u65e0\u6cd5\u52a0\u8f7d\u56fe\u7247&quot;)\n            thumbnail_label.setStyleSheet(&quot;color: red;&quot;)\n        \n        item_layout.addWidget(thumbnail_label)\n        \n        # \u56fe\u7247\u4fe1\u606f\n        info_layout = QVBoxLayout()\n        \n        # \u6587\u4ef6\u540d\u548c\u8def\u5f84\n        filename = os.path.basename(image_path)\n        path_label = QLabel(f&quot;\u6587\u4ef6\u540d: {filename}&quot;)\n        path_label.setStyleSheet(&quot;font-weight: bold;&quot;)\n        \n        dir_label = QLabel(f&quot;\u8def\u5f84: {os.path.dirname(image_path)}&quot;)\n        dir_label.setStyleSheet(&quot;font-size: 11px; color: #555;&quot;)\n        \n        # \u56fe\u7247\u5c3a\u5bf8\u548c\u5927\u5c0f\n        try:\n            img = Image.open(image_path)  # \u91cd\u65b0\u6253\u5f00\u4ee5\u83b7\u53d6\u539f\u59cb\u5c3a\u5bf8\n            size = f&quot;{img.width}\u00d7{img.height}&quot;\n            img_size = os.path.getsize(image_path)\n            size_mb = img_size \/ (1024 * 1024)\n            size_label = QLabel(f&quot;\u5c3a\u5bf8: {size} | \u5927\u5c0f: {size_mb:.2f} MB&quot;)\n        except Exception:\n            size_label = QLabel(&quot;\u5c3a\u5bf8\u4fe1\u606f\u4e0d\u53ef\u7528&quot;)\n        \n        size_label.setStyleSheet(&quot;font-size: 12px;&quot;)\n        \n        info_layout.addWidget(path_label)\n        info_layout.addWidget(dir_label)\n        info_layout.addWidget(size_label)\n        \n        # \u6dfb\u52a0\u5206\u9694\u7ebf\n        if not is_unique:\n            info_layout.addWidget(QLabel())\n            info_layout.addWidget(QLabel(&quot;\u8bf7\u9009\u62e9\u8981\u4fdd\u7559\u7684\u56fe\u7247:&quot;))\n        \n        item_layout.addLayout(info_layout, 1)\n        \n        # \u5bf9\u4e8e\u552f\u4e00\u56fe\u7247\uff0c\u4e0d\u663e\u793a\u9009\u62e9\u6846\n        if is_unique:\n            status_label = QLabel(&quot;\u552f\u4e00\u56fe\u7247\uff0c\u5c06\u81ea\u52a8\u4fdd\u7559&quot;)\n            status_label.setStyleSheet(&quot;color: #27ae60; font-weight: bold;&quot;)\n            item_layout.addWidget(status_label)\n        else:\n            # \u9009\u62e9\u6846 - \u4fee\u590d\u9009\u62e9\u903b\u8f91\n            checkbox = QCheckBox(&quot;\u4fdd\u7559\u6b64\u56fe\u7247&quot;)\n            \n            # \u8bbe\u7f6e\u521d\u59cb\u9009\u4e2d\u72b6\u6001\n            is_selected = image_path in self.selected_images\n            checkbox.setChecked(is_selected)\n            \n            # \u8fde\u63a5\u72b6\u6001\u6539\u53d8\u4fe1\u53f7\n            checkbox.stateChanged.connect(lambda state, path=image_path: self.toggle_selection(path, state))\n            \n            item_layout.addWidget(checkbox)\n        \n        item_widget.setLayout(item_layout)\n        item_widget.setStyleSheet(&quot;&quot;&quot;\n            QWidget {\n                border-bottom: 1px solid #eee;\n                padding: 10px;\n            }\n            QWidget:hover {\n                background-color: #f9f9f9;\n            }\n        &quot;&quot;&quot;)\n        \n        return item_widget\n    \n    def toggle_selection(self, image_path, state):\n        &quot;&quot;&quot;\u5207\u6362\u56fe\u7247\u9009\u62e9\u72b6\u6001\uff08\u4fee\u590d\u9009\u62e9\u903b\u8f91\uff09&quot;&quot;&quot;\n        if state == Qt.Checked:\n            self.selected_images.add(image_path)\n            \n            # \u786e\u4fdd\u540c\u4e00\u7ec4\u4e2d\u53ea\u6709\u4e00\u4e2a\u88ab\u9009\u4e2d\uff08\u5355\u9009\u903b\u8f91\uff09\n            for group in self.duplicate_groups.values():\n                if image_path in group:\n                    # \u53d6\u6d88\u9009\u62e9\u540c\u7ec4\u7684\u5176\u4ed6\u56fe\u7247\n                    for other_path in group:\n                        if other_path != image_path and other_path in self.selected_images:\n                            self.selected_images.remove(other_path)\n                    \n                    # \u66f4\u65b0UI\u663e\u793a\n                    self.display_results()\n                    break\n        else:\n            # \u5982\u679c\u53d6\u6d88\u9009\u62e9\uff0c\u4f46\u8be5\u56fe\u7247\u662f\u7ec4\u4e2d\u552f\u4e00\u88ab\u9009\u4e2d\u7684\uff0c\u5219\u91cd\u65b0\u9009\u4e2d\n            for group in self.duplicate_groups.values():\n                if image_path in group:\n                    # \u68c0\u67e5\u662f\u5426\u8fd8\u6709\u540c\u7ec4\u88ab\u9009\u4e2d\u7684\u56fe\u7247\n                    any_selected = any(p in self.selected_images for p in group)\n                    if not any_selected:\n                        # \u5982\u679c\u6ca1\u6709\u9009\u4e2d\u7684\u56fe\u7247\uff0c\u91cd\u65b0\u9009\u4e2d\u5f53\u524d\u56fe\u7247\n                        self.selected_images.add(image_path)\n                        # \u66f4\u65b0UI\u663e\u793a\n                        self.display_results()\n                        return\n            # \u5982\u679c\u4e0d\u662f\u7ec4\u4e2d\u7684\u56fe\u7247\uff0c\u76f4\u63a5\u79fb\u9664\n            self.selected_images.discard(image_path)\n    \n    def show_image(self, image_path):\n        &quot;&quot;&quot;\u663e\u793a\u5927\u56fe\uff08\u4fee\u590d\u5d29\u6e83\u95ee\u9898\uff09&quot;&quot;&quot;\n        try:\n            viewer = ImageViewerDialog(image_path, self)\n            viewer.exec_()\n        except Exception as e:\n            QMessageBox.critical(self, &quot;\u9519\u8bef&quot;, f&quot;\u65e0\u6cd5\u663e\u793a\u56fe\u7247: {str(e)}&quot;)\n    \n    def export_results(self):\n        &quot;&quot;&quot;\u5bfc\u51fa\u7ed3\u679c\u5230\u8f93\u51fa\u6587\u4ef6\u5939\uff08\u4fee\u590d\u5bfc\u51fa\u95ee\u9898\uff09&quot;&quot;&quot;\n        if not self.output_dir:\n            QMessageBox.warning(self, &quot;\u8b66\u544a&quot;, &quot;\u8bf7\u5148\u9009\u62e9\u8f93\u51fa\u6587\u4ef6\u5939&quot;)\n            return\n        \n        if not self.selected_images:\n            QMessageBox.warning(self, &quot;\u8b66\u544a&quot;, &quot;\u6ca1\u6709\u9009\u62e9\u8981\u4fdd\u7559\u7684\u56fe\u7247&quot;)\n            return\n        \n        try:\n            # \u521b\u5efa\u8f93\u51fa\u76ee\u5f55\n            os.makedirs(self.output_dir, exist_ok=True)\n            \n            # \u590d\u5236\u9009\u4e2d\u7684\u56fe\u7247\n            total = len(self.selected_images)\n            copied = 0\n            \n            # \u663e\u793a\u8fdb\u5ea6\u6761\n            self.progress_bar.setVisible(True)\n            self.progress_bar.setValue(0)\n            \n            for img_path in self.selected_images:\n                filename = os.path.basename(img_path)\n                dest_path = os.path.join(self.output_dir, filename)\n                \n                # \u5904\u7406\u6587\u4ef6\u540d\u51b2\u7a81\n                counter = 1\n                while os.path.exists(dest_path):\n                    name, ext = os.path.splitext(filename)\n                    dest_path = os.path.join(self.output_dir, f&quot;{name}_{counter}{ext}&quot;)\n                    counter += 1\n                \n                shutil.copy2(img_path, dest_path)\n                copied += 1\n                progress = int((copied \/ total) * 100)\n                self.progress_bar.setValue(progress)\n                self.statusBar().showMessage(f&quot;\u6b63\u5728\u5bfc\u51fa: {filename} ({copied}\/{total})&quot;)\n                QApplication.processEvents()  # \u66f4\u65b0UI\n            \n            # \u663e\u793a\u5b8c\u6210\u6d88\u606f\n            QMessageBox.information(\n                self, \n                &quot;\u5bfc\u51fa\u5b8c\u6210&quot;, \n                f&quot;\u6210\u529f\u5bfc\u51fa {copied} \u5f20\u56fe\u7247\u5230:\\n{self.output_dir}&quot;\n            )\n            self.statusBar().showMessage(f&quot;\u5bfc\u51fa\u5b8c\u6210: {copied} \u5f20\u56fe\u7247&quot;)\n            \n        except Exception as e:\n            QMessageBox.critical(\n                self, \n                &quot;\u5bfc\u51fa\u9519\u8bef&quot;, \n                f&quot;\u5bfc\u51fa\u8fc7\u7a0b\u4e2d\u53d1\u751f\u9519\u8bef:\\n{str(e)}&quot;\n            )\n            self.statusBar().showMessage(&quot;\u5bfc\u51fa\u5931\u8d25&quot;)\n        finally:\n            self.progress_bar.setVisible(False)\n\nif __name__ == &quot;__main__&quot;:\n    app = QApplication(sys.argv)\n    app.setStyle(&quot;Fusion&quot;)\n    \n    # \u8bbe\u7f6e\u5e94\u7528\u6837\u5f0f\n    app.setStyleSheet(&quot;&quot;&quot;\n        QMainWindow, QDialog {\n            background-color: #f5f5f5;\n        }\n        QGroupBox {\n            font-weight: bold;\n            border: 1px solid #ddd;\n            border-radius: 5px;\n            margin-top: 10px;\n            padding-top: 15px;\n        }\n        QGroupBox::title {\n            subcontrol-origin: margin;\n            subcontrol-position: top center;\n            padding: 0 5px;\n        }\n        QScrollArea {\n            border: none;\n        }\n        QLabel {\n            padding: 2px;\n        }\n    &quot;&quot;&quot;)\n    \n    window = ImageDedupTool()\n    window.show()\n    sys.exit(app.exec_())\n<\/pre><\/div><\/details>\n\n\n\n<p>\u6e05\u6d17\u597d\u7684\u6570\u636e\u8fd8\u9700\u8981\u8fdb\u884c\u5206\u7ec4\uff0c\u6309\u4e00\u5b9a\u683c\u5f0f\u653e\u5165\u5bf9\u5e94\u7684\u6587\u4ef6\u5939\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u8bad\u7ec3\/\u6d4b\u8bd5<\/h2>\n\n\n\n<p>\u8bad\u7ec3\u7528\u4ee3\u7801\u5982\u4e0b\u3002<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>\uff08\u611f\u8c22deepseek\uff09<\/summary>\n<p><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport torch\nfrom torchvision import datasets, transforms, models\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch.optim as optim\nimport torch.nn as nn\nfrom tqdm import tqdm  # \u7528\u4e8e\u663e\u793a\u8fdb\u5ea6\u6761 (\u53ef\u9009)\n\n\ndef main():\n    # \u5b9a\u4e49\u6570\u636e\u8def\u5f84\n    data_dir = 'Train_test\/images'  # \u89e3\u538b\u540e\u7684\u6839\u76ee\u5f55\n    train_dir = data_dir + '\/train'\n    val_dir = data_dir + '\/val'\n    test_dir = data_dir + '\/test'\n\n    # \u5b9a\u4e49\u6570\u636e\u53d8\u6362 (\u7b80\u5355\u589e\u5f3a)\n    train_transforms = transforms.Compose(&#x5B;\n        transforms.RandomResizedCrop(224),       # \u968f\u673a\u88c1\u526a\u5230 224x224\n        transforms.RandomHorizontalFlip(),       # \u968f\u673a\u6c34\u5e73\u7ffb\u8f6c\n        transforms.ToTensor(),                   # \u8f6c\u4e3a Tensor\n        transforms.Normalize(mean=&#x5B;0.485, 0.456, 0.406], std=&#x5B;0.229, 0.224, 0.225]) # ImageNet \u5f52\u4e00\u5316\n    ])\n\n    val_test_transforms = transforms.Compose(&#x5B;\n        transforms.Resize(256),                 # \u7f29\u653e\u5230 256x256\n        transforms.CenterCrop(224),             # \u4e2d\u5fc3\u88c1\u526a\u5230 224x224\n        transforms.ToTensor(),\n        transforms.Normalize(mean=&#x5B;0.485, 0.456, 0.406], std=&#x5B;0.229, 0.224, 0.225])\n    ])\n\n    # \u52a0\u8f7d\u6570\u636e\u96c6\n    train_dataset = datasets.ImageFolder(train_dir, transform=train_transforms)\n    val_dataset = datasets.ImageFolder(val_dir, transform=val_test_transforms)\n    test_dataset = datasets.ImageFolder(test_dir, transform=val_test_transforms)\n\n    # \u521b\u5efa DataLoader\n    batch_size = 32  # \u6839\u636e\u663e\u5b58\u8c03\u6574\uff0cColab T4 \u7528 32 \u901a\u5e38\u6ca1\u95ee\u9898\n    train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=2)\n    val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=2)\n    test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2)\n\n    # \u67e5\u770b\u7c7b\u522b\u4fe1\u606f (\u975e\u5e38\u91cd\u8981\uff01)\n    class_names = train_dataset.classes\n    print(f'\u8bad\u7ec3\u96c6\u7c7b\u522b: {class_names}')\n    print(f'\u7c7b\u522b\u6570\u91cf: {len(class_names)}')\n\n\n\n    # \u52a0\u8f7d\u9884\u8bad\u7ec3 ResNet50\n    model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)\n\n    # \u51bb\u7ed3\u6240\u6709\u7279\u5f81\u63d0\u53d6\u5c42\u7684\u53c2\u6570 (\u5148\u53ea\u8bad\u7ec3\u65b0\u5934\u90e8)\n    for param in model.parameters():\n        param.requires_grad = False\n\n    # \u66ff\u6362\u6700\u540e\u7684\u5168\u8fde\u63a5\u5c42 (fc)\uff0c\u9002\u914d\u6211\u4eec\u7684\u7c7b\u522b\u6570\n    num_ftrs = model.fc.in_features\n    num_classes = len(class_names)\n    model.fc = torch.nn.Linear(num_ftrs, num_classes)  # \u53ef\u4ee5\u5728\u8fd9\u91cc\u52a0 Dropout (nn.Sequential( nn.Dropout(0.5), nn.Linear(...) ))\n\n    # \u5c06\u6a21\u578b\u79fb\u5230 GPU (\u5982\u679c\u53ef\u7528)\n    device = torch.device(&quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;)\n    model = model.to(device)\n    print(f'\u6a21\u578b\u5df2\u79fb\u81f3: {device}')\n\n\n\n    # \u5b9a\u4e49\u635f\u5931\u51fd\u6570\u548c\u4f18\u5316\u5668 (\u53ea\u4f18\u5316\u65b0\u6dfb\u52a0\u7684 fc \u5c42)\n    criterion = nn.CrossEntropyLoss()\n    optimizer = optim.Adam(model.fc.parameters(), lr=0.001)  # \u5b66\u4e60\u7387 0.001 \u662f\u4e2a\u597d\u8d77\u70b9\n\n    # \u8bad\u7ec3\u8f6e\u6570\n    num_epochs = 10  # \u5c0f\u6570\u636e\u96c6\u901a\u5e38 10-20 \u8f6e\u5c31\u80fd\u770b\u5230\u6548\u679c\n\n    # \u8bb0\u5f55\u8bad\u7ec3\u8fc7\u7a0b\n    train_loss_history = &#x5B;]\n    val_loss_history = &#x5B;]\n    val_acc_history = &#x5B;]\n\n    for epoch in range(num_epochs):\n        # ---------- \u8bad\u7ec3\u9636\u6bb5 ----------\n        model.train()  # \u8bbe\u7f6e\u4e3a\u8bad\u7ec3\u6a21\u5f0f\n        running_loss = 0.0\n\n        # \u4f7f\u7528 tqdm \u5305\u88c5 train_loader \u663e\u793a\u8fdb\u5ea6\u6761\n        for inputs, labels in tqdm(train_loader, desc=f'Epoch {epoch+1}\/{num_epochs} &#x5B;Train]'):\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n\n            # \u6e05\u96f6\u68af\u5ea6\n            optimizer.zero_grad()\n\n            # \u524d\u5411\u4f20\u64ad + \u8ba1\u7b97\u635f\u5931\n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n\n            # \u53cd\u5411\u4f20\u64ad + \u4f18\u5316\n            loss.backward()\n            optimizer.step()\n\n            # \u7edf\u8ba1\u635f\u5931\n            running_loss += loss.item() * inputs.size(0)\n\n        epoch_train_loss = running_loss \/ len(train_dataset)\n\n        # ---------- \u9a8c\u8bc1\u9636\u6bb5 ----------\n        model.eval()  # \u8bbe\u7f6e\u4e3a\u8bc4\u4f30\u6a21\u5f0f\n        val_running_loss = 0.0\n        correct = 0\n        total = 0\n\n        with torch.no_grad():  # \u7981\u7528\u68af\u5ea6\u8ba1\u7b97\uff0c\u8282\u7701\u5185\u5b58\u548c\u8ba1\u7b97\n            for inputs, labels in val_loader:\n                inputs = inputs.to(device)\n                labels = labels.to(device)\n\n                outputs = model(inputs)\n                loss = criterion(outputs, labels)\n                val_running_loss += loss.item() * inputs.size(0)\n\n                _, predicted = torch.max(outputs.data, 1)\n                total += labels.size(0)\n                correct += (predicted == labels).sum().item()\n\n        epoch_val_loss = val_running_loss \/ len(val_dataset)\n        epoch_val_acc = correct \/ total\n\n        # \u8bb0\u5f55\u5386\u53f2\n        train_loss_history.append(epoch_train_loss)\n        val_loss_history.append(epoch_val_loss)\n        val_acc_history.append(epoch_val_acc)\n\n        # \u6253\u5370\u672c\u8f6e\u7ed3\u679c\n        print(f'Epoch {epoch+1}\/{num_epochs} | '\n            f'Train Loss: {epoch_train_loss:.4f} | '\n            f'Val Loss: {epoch_val_loss:.4f} | '\n            f'Val Acc: {epoch_val_acc:.4f}')\n\n        # \u4fdd\u5b58\u9a8c\u8bc1\u96c6\u4e0a\u6700\u597d\u7684\u6a21\u578b (\u7b80\u5355\u5b9e\u73b0)\n        if epoch_val_acc &gt;= max(val_acc_history, default=0):\n            best_model_state = model.state_dict()\n            torch.save(best_model_state, 'insect_resnet50_weights.pth')\n            print(f'** \u4fdd\u5b58\u6700\u4f73\u6a21\u578b (\u51c6\u786e\u7387: {epoch_val_acc:.4f}) **')\n\n    # \u8bad\u7ec3\u7ed3\u675f\uff0c\u52a0\u8f7d\u6700\u4f73\u6a21\u578b\u72b6\u6001\n    model.load_state_dict(best_model_state)\n    print('\u8bad\u7ec3\u5b8c\u6210\uff01\u52a0\u8f7d\u4e86\u9a8c\u8bc1\u96c6\u4e0a\u8868\u73b0\u6700\u597d\u7684\u6a21\u578b\u3002')\n\n\n    model.eval()\n    test_correct = 0\n    test_total = 0\n    class_correct = &#x5B;0] * num_classes\n    class_total = &#x5B;0] * num_classes\n\n    confusion_matrix = torch.zeros(num_classes, num_classes)  # \u521d\u59cb\u5316\u6df7\u6dc6\u77e9\u9635\n\n    with torch.no_grad():\n        for inputs, labels in test_loader:\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n\n            outputs = model(inputs)\n            _, predicted = torch.max(outputs, 1)\n\n            test_total += labels.size(0)\n            test_correct += (predicted == labels).sum().item()\n\n            # \u8ba1\u7b97\u6bcf\u4e2a\u7c7b\u7684\u6b63\u786e\u6570\u548c\u603b\u6570\uff0c\u4ee5\u53ca\u6df7\u6dc6\u77e9\u9635\n            for i in range(len(labels)):\n                label = labels&#x5B;i]\n                pred = predicted&#x5B;i]\n                class_correct&#x5B;label] += (pred == label).item()\n                class_total&#x5B;label] += 1\n                confusion_matrix&#x5B;label]&#x5B;pred] += 1  # \u6ce8\u610f\uff1a\u884c\u662f\u771f\u5b9e\u6807\u7b7e\uff0c\u5217\u662f\u9884\u6d4b\u6807\u7b7e\n\n    # \u6253\u5370\u603b\u4f53\u6d4b\u8bd5\u51c6\u786e\u7387\n    test_acc = test_correct \/ test_total\n    print(f'\u6d4b\u8bd5\u96c6\u51c6\u786e\u7387: {test_acc:.4f}')\n\n    # (\u53ef\u9009) \u6253\u5370\u6bcf\u4e2a\u7c7b\u522b\u7684\u51c6\u786e\u7387\n    print('\\n\u6bcf\u4e2a\u7c7b\u522b\u7684\u51c6\u786e\u7387:')\n    for i in range(num_classes):\n        if class_total&#x5B;i] &gt; 0:\n            acc = class_correct&#x5B;i] \/ class_total&#x5B;i]\n            print(f'  {class_names&#x5B;i]}: {acc:.4f} ({class_correct&#x5B;i]}\/{class_total&#x5B;i]})')\n        else:\n            print(f'  {class_names&#x5B;i]}: \u65e0\u6837\u672c')\n\n    # (\u53ef\u9009) \u53ef\u89c6\u5316\u6df7\u6dc6\u77e9\u9635 (\u9700\u8981 matplotlib \u548c seaborn)\n    &quot;&quot;&quot; import seaborn as sns\n    plt.figure(figsize=(10, 8))\n    sns.heatmap(confusion_matrix.numpy(), annot=True, fmt='g', cmap='Blues',\n                xticklabels=class_names, yticklabels=class_names)\n    plt.xlabel('\u9884\u6d4b\u6807\u7b7e')\n    plt.ylabel('\u771f\u5b9e\u6807\u7b7e')\n    plt.title('\u6df7\u6dc6\u77e9\u9635')\n    plt.tight_layout()\n    plt.show() &quot;&quot;&quot;\n\n\n    def predict_image(image_path, model, class_names, transform):\n        &quot;&quot;&quot;\n        \u9884\u6d4b\u5355\u5f20\u56fe\u50cf\u7684\u7c7b\u522b\n        &quot;&quot;&quot;\n        model.eval()\n        image = Image.open(image_path).convert('RGB')  # \u786e\u4fdd\u662f RGB\n        image_tensor = transform(image).unsqueeze(0)  # \u589e\u52a0 batch \u7ef4\u5ea6 &#x5B;1, C, H, W]\n        image_tensor = image_tensor.to(device)\n\n        with torch.no_grad():\n            output = model(image_tensor)\n            probabilities = torch.nn.functional.softmax(output&#x5B;0], dim=0)  # \u8ba1\u7b97\u6982\u7387\n            top_prob, top_class_idx = torch.topk(probabilities, k=3)  # \u53d6 Top-3\n\n        # \u6253\u5370\u7ed3\u679c\n        print(f'\u9884\u6d4b\u56fe\u50cf: {image_path}')\n        plt.imshow(image)\n        plt.axis('off')\n        plt.show()\n        for i in range(top_prob.size(0)):\n            class_idx = top_class_idx&#x5B;i].item()\n            print(f'  Top-{i+1}: {class_names&#x5B;class_idx]} - \u6982\u7387: {top_prob&#x5B;i].item():.4f}')\n\n    # \u4f7f\u7528\u6d4b\u8bd5\u96c6\u4e2d\u7684\u4e00\u5f20\u56fe\u7247\u6216\u4e0a\u4f20\u65b0\u56fe\u7247\u6d4b\u8bd5\n    # \u793a\u4f8b\uff1a\u9884\u6d4b\u6d4b\u8bd5\u96c6\u7b2c\u4e00\u5f20\u56fe\u7247\n    sample_image_path = test_dataset.samples&#x5B;0]&#x5B;0]  # \u83b7\u53d6\u7b2c\u4e00\u5f20\u6d4b\u8bd5\u56fe\u7247\u7684\u8def\u5f84\n    predict_image(sample_image_path, model, class_names, val_test_transforms)\n\n\nif __name__ == '__main__':\n    # \u5728 Windows \u4e0a\u53ef\u80fd\u9700\u8981\u6dfb\u52a0\u8fd9\u884c\n    torch.multiprocessing.freeze_support()\n    main()\n<\/pre><\/div><\/details>\n\n\n\n<h2 class=\"wp-block-heading\">\u5176\u4ed6<\/h2>\n\n\n\n<p>\u65e5\u5e38\u751f\u6d3b\u4e2d\u4eba\u4eec\u62cd\u6444\u7684\u7167\u7247\u5927\u591a\u90fd\u4e0d\u7b26\u5408\u6a21\u578b\u8f93\u5165\u7684\u8981\u6c42\uff0c\u6bd4\u5982\u4e00\u5f20\u56fe\u7247\u4e2d\u6606\u866b\u53ef\u80fd\u53ea\u5360\u636e\u4e86\u4e00\u5c0f\u90e8\u5206\u4f4d\u7f6e\uff0c\u8bc6\u522b\u6a21\u578b\u7684\u88c1\u5207\u65b9\u5f0f\u53ef\u80fd\u4f1a\u5bfc\u81f4\u90e8\u5206\u4fe1\u606f\u4e22\u5931\uff0c\u9020\u6210\u8bc6\u522b\u9519\u8bef\uff1b\u53c8\u6bd4\u5982\u4e00\u5f20\u56fe\u7247\u4e2d\u6606\u866b\u4e0d\u6b62\u4e00\u4e2a\u7b49\u7b49\u3002\u56e0\u6b64\u9664\u4e86\u8bc6\u522b\u7cfb\u7edf\u5916\u6211\u4eec\u8fd8\u9700\u8981\u4e00\u4e2a\u68c0\u6d4b\u6846\u9009\u7cfb\u7edf\uff0c\u8fd9\u90e8\u5206\u53ef\u7528yolo\u6216Faster R-CNN\u5b9e\u73b0\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u6570\u636e\u641c\u96c6 \u5728Kaggle\uff08Find Open Datasets and Machine Learning Pr [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11],"tags":[12,7,20],"class_list":["post-174","post","type-post","status-publish","format-standard","hentry","category-11","tag-ai","tag-python","tag-20"],"_links":{"self":[{"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/posts\/174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/comments?post=174"}],"version-history":[{"count":4,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/posts\/174\/revisions"}],"predecessor-version":[{"id":178,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/posts\/174\/revisions\/178"}],"wp:attachment":[{"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/media?parent=174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/categories?post=174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.sayoung.wang\/index.php\/wp-json\/wp\/v2\/tags?post=174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}